Skip to content

Commit 7b8967a

Browse files
authored
Merge pull request #81 from Final-Project-Team-Temporary/feature/사용자-이름-추가
Feat 퀴즈 생성 오류 수정
2 parents 441645b + 43ec4ff commit 7b8967a

6 files changed

Lines changed: 144 additions & 11 deletions

File tree

src/main/java/com/example/whiplash/global/config/ObjectMapperConfig.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,45 @@
22

33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.context.annotation.Primary;
56

67
import com.fasterxml.jackson.annotation.JsonTypeInfo;
8+
import com.fasterxml.jackson.databind.DeserializationFeature;
79
import com.fasterxml.jackson.databind.ObjectMapper;
810
import com.fasterxml.jackson.databind.SerializationFeature;
11+
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
12+
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
913
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
1014
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
1115

1216
@Configuration
1317
public class ObjectMapperConfig {
18+
19+
/**
20+
* HTTP Request/Response용 ObjectMapper (Primary)
21+
*/
1422
@Bean
15-
ObjectMapper objectMapper() {
16-
// ObjectMapper 커스터마이징
23+
@Primary
24+
public ObjectMapper objectMapper() {
1725
ObjectMapper objectMapper = new ObjectMapper();
1826

1927
setLocalDateTimeModule(objectMapper);
28+
setDeserializationConfig(objectMapper);
29+
// ⭐ HTTP용은 타입 정보 비활성화 (클라이언트 JSON에 @class 필드 없음)
30+
31+
return objectMapper;
32+
}
33+
34+
/**
35+
* Redis 직렬화/역직렬화 전용 ObjectMapper
36+
*/
37+
@Bean("redisObjectMapper")
38+
public ObjectMapper redisObjectMapper() {
39+
ObjectMapper objectMapper = new ObjectMapper();
40+
41+
setLocalDateTimeModule(objectMapper);
42+
setDeserializationConfig(objectMapper);
43+
setPolymorphicTypeValidation(objectMapper); // ⭐ Redis용은 타입 정보 활성화
2044

2145
return objectMapper;
2246
}
@@ -25,4 +49,24 @@ private static void setLocalDateTimeModule(ObjectMapper objectMapper) {
2549
objectMapper.registerModule(new JavaTimeModule()); // ISO 8601로 직렬화
2650
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // LocalDateTime을 timestamp 형식으로 직렬화 안함
2751
}
52+
53+
private static void setDeserializationConfig(ObjectMapper objectMapper) {
54+
// 알 수 없는 속성이 있어도 무시 (역직렬화 시 유연성 - Redis에서 필요)
55+
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
56+
}
57+
58+
private static void setPolymorphicTypeValidation(ObjectMapper objectMapper) {
59+
// Redis 역직렬화를 위한 타입 정보 활성화
60+
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
61+
.allowIfBaseType(Object.class)
62+
.allowIfBaseType(java.util.List.class)
63+
.allowIfBaseType(java.util.ArrayList.class)
64+
.build();
65+
66+
objectMapper.activateDefaultTyping(
67+
ptv,
68+
ObjectMapper.DefaultTyping.NON_FINAL,
69+
JsonTypeInfo.As.PROPERTY
70+
);
71+
}
2872
}

src/main/java/com/example/whiplash/global/config/RedisConfig.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.whiplash.global.config;
22

3+
import org.springframework.beans.factory.annotation.Qualifier;
34
import org.springframework.beans.factory.annotation.Value;
45
import org.springframework.context.annotation.Bean;
56
import org.springframework.context.annotation.Configuration;
@@ -18,7 +19,6 @@
1819
@RequiredArgsConstructor
1920
@Configuration
2021
public class RedisConfig {
21-
private final ObjectMapper objectMapper;
2222

2323
@Bean
2424
public RedisConnectionFactory redisConnectionFactory(
@@ -42,14 +42,17 @@ public RedisConnectionFactory redisConnectionFactory(
4242
}
4343

4444
@Bean
45-
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
45+
public RedisTemplate<String, Object> redisTemplate(
46+
RedisConnectionFactory factory,
47+
@Qualifier("redisObjectMapper") ObjectMapper redisObjectMapper) {
48+
4649
RedisTemplate<String, Object> template = new RedisTemplate<>();
4750
template.setConnectionFactory(factory);
4851

4952
// Key는 StringSerializer
5053
StringRedisSerializer stringSerializer = new StringRedisSerializer();
51-
// Value는 JSON
52-
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
54+
// Value는 JSON (Redis 전용 ObjectMapper 사용)
55+
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(redisObjectMapper);
5356

5457
template.setKeySerializer(stringSerializer);
5558
template.setValueSerializer(jsonSerializer);

src/main/java/com/example/whiplash/quiz/client/AiServerClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public QuizResDto generateQuiz(String keyword, int count) {
4747
return Mono.error(new RuntimeException("AI 서버 응답 실패"));
4848
})
4949
.bodyToMono(QuizResDto.class)
50-
.timeout(Duration.ofSeconds(10))
50+
.timeout(Duration.ofSeconds(100))
5151
.block(); // 동기 방식으로 대기
5252

5353
if (response != null) {

src/main/java/com/example/whiplash/quiz/controller/QuizController.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,29 @@ public ApiResponse<WeeklyChallengeResDto.MyAttemptInfo> submitChallenge(
147147

148148
return ApiResponse.onSuccess(result);
149149
}
150+
151+
/**
152+
* ⭐ 퀴즈 캐시 초기화 (디버깅용)
153+
*/
154+
@DeleteMapping("/cache")
155+
@Operation(summary = "퀴즈 캐시 초기화", description = "사용자의 모든 퀴즈 캐시를 삭제합니다 (역직렬화 오류 해결용)")
156+
public ApiResponse<String> clearQuizCache(
157+
@AuthenticationPrincipal UserPrincipal principal,
158+
@RequestParam(required = false) String term
159+
) {
160+
161+
Long userId = principal.getUserId();
162+
163+
if (term != null && !term.isBlank()) {
164+
// 특정 용어의 캐시만 삭제
165+
mixedQuizService.clearTermQuizCache(userId, term);
166+
log.info("용어 퀴즈 캐시 삭제: userId={}, term={}", userId, term);
167+
return ApiResponse.onSuccess("용어 '" + term + "'의 캐시가 삭제되었습니다.");
168+
} else {
169+
// 모든 퀴즈 캐시 삭제
170+
mixedQuizService.clearUserQuizCache(userId);
171+
log.info("모든 퀴즈 캐시 삭제: userId={}", userId);
172+
return ApiResponse.onSuccess("모든 퀴즈 캐시가 삭제되었습니다.");
173+
}
174+
}
150175
}

src/main/java/com/example/whiplash/quiz/dto/response/QuizResDto.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.example.whiplash.quiz.dto.response;
22

33
import com.example.whiplash.quiz.dto.QuizDto;
4+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
45
import lombok.AllArgsConstructor;
56
import lombok.Data;
67
import lombok.NoArgsConstructor;
@@ -12,6 +13,7 @@
1213
@Data
1314
@NoArgsConstructor
1415
@AllArgsConstructor
16+
@JsonIgnoreProperties(ignoreUnknown = true)
1517
public class QuizResDto implements Serializable {
1618

1719
private List<QuizDto> quizzes;

src/main/java/com/example/whiplash/quiz/service/MixedQuizService.java

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,43 @@ private List<QuizDto> getQuizzesForTerm(Long userId, String term) {
8383
String cacheKey = CACHE_KEY_PREFIX + userId + ":" + term;
8484

8585
// 1. 캐시 확인
86-
QuizResDto cached = (QuizResDto) redisTemplate.opsForValue().get(cacheKey);
86+
Object cachedData = redisTemplate.opsForValue().get(cacheKey);
8787

88-
if (cached != null) {
88+
if (cachedData != null) {
8989
log.debug("캐시 히트: userId={}, term={}", userId, term);
90-
return cached.getQuizzes();
90+
91+
try {
92+
// Redis에서 QuizResDto로 역직렬화 (타입 정보 포함)
93+
if (cachedData instanceof QuizResDto) {
94+
QuizResDto cached = (QuizResDto) cachedData;
95+
log.info("캐싱 데이터: term={}, count={}", term, cached.getQuizzes().size());
96+
return cached.getQuizzes();
97+
} else {
98+
log.warn("예상치 못한 캐시 타입: {}", cachedData.getClass().getName());
99+
// 잘못된 타입이면 캐시 삭제 후 재생성
100+
redisTemplate.delete(cacheKey);
101+
return generateAndCacheQuiz(userId, term, cacheKey);
102+
}
103+
} catch (Exception e) {
104+
// 역직렬화 실패 시 캐시 삭제 후 재생성
105+
log.warn("캐시 역직렬화 실패 - 캐시 삭제 후 재생성: userId={}, term={}, error={}",
106+
userId, term, e.getMessage());
107+
redisTemplate.delete(cacheKey);
108+
109+
// 재생성 후 반환
110+
return generateAndCacheQuiz(userId, term, cacheKey);
111+
}
91112
}
92113

93114
// 2. 캐시 미스 - AI 서버 실시간 생성
94115
log.warn("캐시 미스 - 실시간 생성: userId={}, term={}", userId, term);
116+
return generateAndCacheQuiz(userId, term, cacheKey);
117+
}
95118

119+
/**
120+
* 퀴즈 생성 및 캐싱 (공통 로직 분리)
121+
*/
122+
private List<QuizDto> generateAndCacheQuiz(Long userId, String term, String cacheKey) {
96123
try {
97124
QuizResDto freshQuiz = aiServerClient.generateQuiz(term, 3);
98125

@@ -103,11 +130,14 @@ private List<QuizDto> getQuizzesForTerm(Long userId, String term) {
103130
java.time.Duration.ofDays(7)
104131
);
105132

133+
log.info("퀴즈 생성 및 캐싱 완료: userId={}, term={}, count={}",
134+
userId, term, freshQuiz.getQuizzes().size());
135+
106136
return freshQuiz.getQuizzes();
107137

108138
} catch (Exception e) {
109139
log.error("퀴즈 생성 실패: userId={}, term={}", userId, term, e);
110-
throw new RuntimeException("퀴즈를 생성할 수 없습니다: " + term);
140+
throw new RuntimeException("퀴즈를 생성할 수 없습니다: " + term, e);
111141
}
112142
}
113143

@@ -132,4 +162,33 @@ private List<QuizDto> selectRandomQuizzes(List<QuizDto> quizzes, int count) {
132162
private int calculateEstimatedTime(int totalQuestions) {
133163
return (int) Math.ceil(totalQuestions * 0.5); // 0.5분 = 30초
134164
}
165+
166+
/**
167+
* 특정 사용자의 모든 퀴즈 캐시 삭제
168+
*/
169+
public void clearUserQuizCache(Long userId) {
170+
String pattern = CACHE_KEY_PREFIX + userId + ":*";
171+
172+
// 패턴에 맞는 키 찾기
173+
var keys = redisTemplate.keys(pattern);
174+
175+
if (keys != null && !keys.isEmpty()) {
176+
redisTemplate.delete(keys);
177+
log.info("퀴즈 캐시 삭제 완료: userId={}, count={}", userId, keys.size());
178+
} else {
179+
log.info("삭제할 캐시가 없음: userId={}", userId);
180+
}
181+
}
182+
183+
/**
184+
* 특정 용어의 퀴즈 캐시 삭제
185+
*/
186+
public void clearTermQuizCache(Long userId, String term) {
187+
String cacheKey = CACHE_KEY_PREFIX + userId + ":" + term;
188+
189+
Boolean deleted = redisTemplate.delete(cacheKey);
190+
191+
log.info("용어 퀴즈 캐시 삭제: userId={}, term={}, deleted={}",
192+
userId, term, deleted);
193+
}
135194
}

0 commit comments

Comments
 (0)