From fcd78c4ee3ec6ad38aa699e0df8db448901b465a Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 00:30:25 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20Redis=20=EC=A7=81=EB=A0=AC=ED=99=94?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/config/RedisCacheConfig.kt | 52 ++++--------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt b/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt index 0de44a5..6610bbf 100644 --- a/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt +++ b/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt @@ -1,10 +1,7 @@ package com.stepbookstep.server.global.config -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import org.springframework.cache.CacheManager import org.springframework.cache.annotation.EnableCaching @@ -13,9 +10,8 @@ import org.springframework.context.annotation.Configuration import org.springframework.data.redis.cache.RedisCacheConfiguration import org.springframework.data.redis.cache.RedisCacheManager import org.springframework.data.redis.connection.RedisConnectionFactory -import org.springframework.data.redis.serializer.RedisSerializer +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer import org.springframework.data.redis.serializer.RedisSerializationContext -import org.springframework.data.redis.serializer.SerializationException import org.springframework.data.redis.serializer.StringRedisSerializer import java.time.Duration @@ -25,40 +21,9 @@ class RedisCacheConfig { @Bean fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager { - val objectMapper = ObjectMapper() - objectMapper.registerModule(JavaTimeModule()) - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - objectMapper.activateDefaultTyping( - BasicPolymorphicTypeValidator.builder() - .allowIfBaseType(Any::class.java) - .build(), - ObjectMapper.DefaultTyping.NON_FINAL, - JsonTypeInfo.As.PROPERTY - ) - - val serializer = object : RedisSerializer { - override fun serialize(t: Any?): ByteArray { - if (t == null) { - return ByteArray(0) - } - try { - return objectMapper.writeValueAsBytes(t) - } catch (e: Exception) { - throw SerializationException("Could not write JSON: " + e.message, e) - } - } - - override fun deserialize(bytes: ByteArray?): Any? { - if (bytes == null || bytes.isEmpty()) { - return null - } - try { - return objectMapper.readValue(bytes, Any::class.java) - } catch (e: Exception) { - throw SerializationException("Could not read JSON: " + e.message, e) - } - } + val objectMapper = ObjectMapper().apply { + registerModule(JavaTimeModule()) + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) } val defaultConfig = RedisCacheConfiguration.defaultCacheConfig() @@ -67,14 +32,19 @@ class RedisCacheConfig { RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()) ) .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer(serializer) + RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer(objectMapper)) ) val cacheConfigurations = mapOf( "genreBooks" to defaultConfig.entryTtl(Duration.ofHours(6)), "bestsellerBooks" to defaultConfig.entryTtl(Duration.ofHours(12)), "bookDetail" to defaultConfig.entryTtl(Duration.ofHours(24)), - "booksByLevel" to defaultConfig.entryTtl(Duration.ofHours(1)) + "booksByLevel" to defaultConfig.entryTtl(Duration.ofHours(1)), + "userStatistics" to defaultConfig.entryTtl(Duration.ofMinutes(30)), + "distinctCategoryIds" to defaultConfig.entryTtl(Duration.ofHours(24)), + "distinctGenreIds" to defaultConfig.entryTtl(Duration.ofHours(24)), + "categoryBooks" to defaultConfig.entryTtl(Duration.ofHours(6)), + "genreIdBooks" to defaultConfig.entryTtl(Duration.ofHours(6)) ) return RedisCacheManager.builder(redisConnectionFactory) From f5830ce96281acd22c6e8e584de48ea1d5bf69df Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 00:30:01 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20@Cacheable=20=EC=A0=81=EC=9A=A9=20(?= =?UTF-8?q?userStatistics)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/reading/application/StatisticService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/application/StatisticService.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/application/StatisticService.kt index 68e10e5..add989c 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/reading/application/StatisticService.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/reading/application/StatisticService.kt @@ -3,6 +3,7 @@ package com.stepbookstep.server.domain.reading.application import com.stepbookstep.server.domain.book.domain.BookRepository import com.stepbookstep.server.domain.reading.domain.* import com.stepbookstep.server.domain.reading.presentation.dto.* +import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -21,6 +22,7 @@ class StatisticsService( /** * 전체 독서 통계 조회 */ + @Cacheable(value = ["userStatistics"], key = "#userId + '_' + #year") @Transactional(readOnly = true) fun getReadingStatistics(userId: Long, year: Int): ReadingStatisticsResponse { return ReadingStatisticsResponse( From 15fa8c39f45ea75612411923a56ba5a982036ef0 Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 00:29:27 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/reading/application/ReadingGoalService.kt | 3 +++ .../server/domain/reading/application/ReadingLogService.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingGoalService.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingGoalService.kt index eef2de4..10b8986 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingGoalService.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingGoalService.kt @@ -12,6 +12,7 @@ import com.stepbookstep.server.domain.reading.domain.UserBook import com.stepbookstep.server.domain.reading.domain.UserBookRepository import com.stepbookstep.server.global.response.CustomException import com.stepbookstep.server.global.response.ErrorCode +import org.springframework.cache.annotation.CacheEvict import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -30,6 +31,7 @@ class ReadingGoalService( * - 기존 활성 목표가 없으면 새로 생성 * - 기존 활성 목표가 있으면 수정 */ + @CacheEvict(value = ["userStatistics"], key = "#userId + '_' + T(java.time.Year).now().value") @Transactional fun upsertGoal( userId: Long, @@ -96,6 +98,7 @@ class ReadingGoalService( /** * 활성 목표 삭제 (비활성화) */ + @CacheEvict(value = ["userStatistics"], key = "#userId + '_' + T(java.time.Year).now().value") @Transactional fun deleteGoal(userId: Long, bookId: Long) { val existingGoal = readingGoalRepository.findByUserIdAndBookIdAndActiveTrue(userId, bookId) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingLogService.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingLogService.kt index a21a195..eb323f0 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingLogService.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/reading/application/ReadingLogService.kt @@ -15,6 +15,7 @@ import com.stepbookstep.server.domain.reading.presentation.dto.GoalInfo import com.stepbookstep.server.domain.reading.presentation.dto.ReadingLogItem import com.stepbookstep.server.global.response.CustomException import com.stepbookstep.server.global.response.ErrorCode +import org.springframework.cache.annotation.CacheEvict import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -28,6 +29,7 @@ class ReadingLogService( private val readingGoalRepository: ReadingGoalRepository ) { + @CacheEvict(value = ["userStatistics"], key = "#userId + '_' + T(java.time.Year).now().value") @Transactional fun createLog( userId: Long, From b946a89bce7af7189f3f110531fa7a9ea8496d6f Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 00:28:46 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor:=20=ED=99=88=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/home/application/HomeQueryService.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeQueryService.kt b/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeQueryService.kt index 9effbcf..d19c3f5 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeQueryService.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeQueryService.kt @@ -101,12 +101,9 @@ class HomeQueryService( } private fun selectRandomCategoryOrGenre(): SelectedBooks { - val allBooks = bookRepository.findAll() - - // categoryId가 있는 책들의 고유 categoryId 목록 - val categoryIds = allBooks.mapNotNull { it.categoryId }.distinct() - // genreId가 있는 책들의 고유 genreId 목록 - val genreIds = allBooks.mapNotNull { it.genreId }.distinct() + // 캐시된 DISTINCT ID 목록 조회 + val categoryIds = homeCacheService.getDistinctCategoryIds() + val genreIds = homeCacheService.getDistinctGenreIds() val allOptions = mutableListOf>() categoryIds.forEach { allOptions.add("category" to it) } @@ -118,8 +115,8 @@ class HomeQueryService( val selected = allOptions.random() val books = when (selected.first) { - "category" -> allBooks.filter { it.categoryId == selected.second } - "genre" -> allBooks.filter { it.genreId == selected.second } + "category" -> homeCacheService.getBooksByCategoryId(selected.second) + "genre" -> homeCacheService.getBooksByGenreId(selected.second) else -> emptyList() }.shuffled().take(20) From 89f6d61011e306625334afd5c6f9cf1733c9c021 Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 00:28:07 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=EC=BA=90=EC=8B=9C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/home/application/HomeCacheService.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeCacheService.kt b/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeCacheService.kt index 4ad4b20..8b08316 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeCacheService.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/home/application/HomeCacheService.kt @@ -41,4 +41,14 @@ class HomeCacheService( fun getBooksByGenreId(genreId: Long): List { return bookRepository.findAllByGenreId(genreId) } + + @Cacheable(value = ["distinctCategoryIds"]) + fun getDistinctCategoryIds(): List { + return bookRepository.findDistinctCategoryIds() + } + + @Cacheable(value = ["distinctGenreIds"]) + fun getDistinctGenreIds(): List { + return bookRepository.findDistinctGenreIds() + } } From 96e26f84d076923a4da1672c00d3af5a913ade1f Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 00:27:39 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20(categoryId,=20genreI?= =?UTF-8?q?d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/book/domain/BookRepository.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/book/domain/BookRepository.kt b/src/main/kotlin/com/stepbookstep/server/domain/book/domain/BookRepository.kt index 11ba706..e4737b8 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/book/domain/BookRepository.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/book/domain/BookRepository.kt @@ -1,7 +1,5 @@ package com.stepbookstep.server.domain.book.domain -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaSpecificationExecutor import org.springframework.data.jpa.repository.Query @@ -41,4 +39,10 @@ interface BookRepository : JpaRepository, JpaSpecificationExecutor + + @Query("SELECT DISTINCT b.categoryId FROM Book b") + fun findDistinctCategoryIds(): List + + @Query("SELECT DISTINCT b.genreId FROM Book b WHERE b.genreId IS NOT NULL") + fun findDistinctGenreIds(): List } From 6384dc5f60e469775965e48cd9c384beab07ee74 Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 14:12:13 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20KotlinModule=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8c2425d..73a3d89 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { // Kotlin implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("tools.jackson.module:jackson-module-kotlin") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") // Jackson Java 8 Date/Time implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") From 35b3aa3bf87538f63b57fd465e6e73b2e3794444 Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 14:12:56 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20isBestseller=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=A7=81=EB=A0=AC=ED=99=94/=EC=97=AD=EC=A7=81=EB=A0=AC?= =?UTF-8?q?=ED=99=94=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/stepbookstep/server/domain/book/domain/Book.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/book/domain/Book.kt b/src/main/kotlin/com/stepbookstep/server/domain/book/domain/Book.kt index e5e2469..e5933bc 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/book/domain/Book.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/book/domain/Book.kt @@ -1,5 +1,6 @@ package com.stepbookstep.server.domain.book.domain +import com.fasterxml.jackson.annotation.JsonProperty import jakarta.persistence.* import java.time.LocalDate import java.time.LocalDateTime @@ -82,6 +83,7 @@ class Book( val vocabLevel: VocabLevel = VocabLevel.EASY, @Column(name = "is_bestseller", nullable = false) + @param:JsonProperty("isBestseller") val isBestseller: Boolean = false, // ===== 시스템 필드 ===== From 185cf1692fcb06e4224fa5785e5171812e2d11eb Mon Sep 17 00:00:00 2001 From: eun-seoo Date: Wed, 11 Feb 2026 14:16:44 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20Redis=20=EC=A7=81=EB=A0=AC=ED=99=94?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20Jackso?= =?UTF-8?q?n=20ObjectMapper=20=EA=B5=AC=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/config/RedisCacheConfig.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt b/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt index 6610bbf..a9e7996 100644 --- a/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt +++ b/src/main/kotlin/com/stepbookstep/server/global/config/RedisCacheConfig.kt @@ -1,8 +1,12 @@ package com.stepbookstep.server.global.config +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.cache.CacheManager import org.springframework.cache.annotation.EnableCaching import org.springframework.context.annotation.Bean @@ -21,9 +25,15 @@ class RedisCacheConfig { @Bean fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager { - val objectMapper = ObjectMapper().apply { + val objectMapper: ObjectMapper = jacksonObjectMapper().apply { registerModule(JavaTimeModule()) disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + activateDefaultTyping( + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY + ) } val defaultConfig = RedisCacheConfiguration.defaultCacheConfig()