ํ๋ก์ ํธ ์ค ์ต๊ทผ ๊ฒ์ ๊ธฐ๋ก์ ๊ตฌํํด์ผ ํ ์ผ์ด ์๊ฒผ๋ค. ์ฒ์์ SearchLog ์ํฐํฐ๋ฅผ ๋ง๋ค์ด mysql์ ์ ์ฅํ ๊น ํ๋ค.
ํ์ง๋ง ๊ฒ์ ์ ํญ์ ๊ฒ์ ๊ธฐ๋ก์ ์กฐํํด์์ผ ํ๊ณ ๋ค. ํนํ ๋๊ฐ์ ๊ฒฝ์ฐ ๊ฒ์ ๊ธฐ๋ก ๊ฐ์๋ฅผ 10๊ฐ๋ก ์ ํํด๋์๋๋ฐ, ๋ง์ฝ ๊ฒ์ ๊ธฐ๋ก์ด ๊ฝ ์ฐผ์ ๋ ์๋ก insert ํ๊ธฐ ์ํด์ ๊ฐ์ฅ ์ค๋๋ ๊ธฐ๋ก์ ์ ๊ฑฐํด์ผ ํ๊ธฐ ๋๋ฌธ์ ์ ๋ ฌ ํ delete ํ๋ ๊ณผ์ ์ด ํ์ํ๋ค.
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ธ๋ฉ๋ชจ๋ฆฌ DB์ธ Redis๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค.
๋ ๋์ค ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ ๊ฒ์ด๊ณ SearchLog + memberId ๋ฅผ key๋ก ๋์ด ์๋ณํ ์ ์๋๋ก ๊ตฌํํ๋ค.
๋ ๋์ค ๋ฆฌ์คํธ๋ฅผ ๋ค๋ฃจ๊ธฐ ์ํ ๋ฉ์๋๋ leftpush,leftpop,rightpush,rightpop ๋ฑ์ด ์์ด ํ์ฒ๋ผ ์ด์ฉํ ์ ์๋ค.
์ธ๋ฉ๋ชจ๋ฆฌ??
์ปดํจํฐ์ ๋ฉ์ธ ๋ฉ๋ชจ๋ฆฌ RAM์ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ ค์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋งํ๋ค. ์ฌ์ฉํ๋ ์ด์ ๋ ๋ช ํํ๊ฒ ์๋ ๋๋ฌธ์ด๋ค. SSD, HDD ๊ฐ์ ์ ์ฅ๊ณต๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ๋ณด๋ค RAM์ ์ฌ๋ ค์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ด ์๋ฐฑ๋ฐฐ(HDD ๊ธฐ์ค) ์ด์ ๋น ๋ฅด๋ค. ์ฆ, Redis๋ ๋น ๋ฅธ ๊ฒ์ ์๋๋ฅผ ๊ฐ์ง๋ค.
ํ์ง๋ง ๋น ๋ฅธ ์๋๋ฅผ ๊ฐ์ง๋๋งํผ ๋จ์ ๋ ์กด์ฌํ๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ๋ ธํธ๋ถ ์ฌ์์ ๊ณ ๋ฅผ ๋ RAM ์ฉ๋์ด 8๊ธฐ๊ฐ, 16๊ธฐ๊ฐ 32๊ธฐ๊ฐ๋ก ๋ง์ด ์์ฉ๋๊ณ ์๋๋ฐ ๋จ์ด ์ปค์ง์๋ก ๋น์ฉ์ด ์ฆ๊ฐํ๋ ๊ฒ์ ๊ฒฝํํด๋ณธ ์ ์ด ์์ ๊ฒ์ด๋ค. ๋ํ Key-Value ํํ์ NoSQL์ด๋ผ๋ ์ ์์ ๋ค์ํ ํํ์ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ง์ํ๊ธฐ๋ ํ์ง๋ง ๋ณต์กํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ์ฌ์ฉํ๊ธฐ์๋ ์ด๋ ค์์ด ์๋ค.
์ฝ๋๋ก ์ดํด๋ณด์.
RedisConfig์ SearchLog๋ฅผ ์ ์ฅํ ์ ์๋ RedisTemplate๋ฅผ ๋ฑ๋กํด์ค์ผ ํ๋ค.
@Configuration
@EnableCaching
public class RedisConfig {
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
// ๊ฒ์ ๋ก๊ทธ ํ
ํ๋ฆฟ
@Bean
public RedisTemplate<String, SearchLog> SearchLogRedis() {
RedisTemplate<String, SearchLog> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(SearchLog.class));
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(SearchLog.class));
return redisTemplate;
}
}
key ๋ ์์์ ๋งํ๋ฏ์ด SearchLog + [memberId] ๋ก ์๋ณํ๊ณ value๋ [๊ฒ์์ด, ์์ฑ์๊ฐ] ์ผ๋ก ์ ์ฅํ ๊ฒ์ด๋ค.
๋จผ์ value์ ํด๋นํ๋ SearchLog ํด๋์ค๋ฅผ ์์ฑํด์ฃผ์.
์ํฐํฐ์๋ค๋ฉด @AllArgsConstructor ์ด๋ ํด๋์ค๋จ์ @Builder ์ด๋ ธํ ์ด์ ์ ๋ถ์ด์ง ์๋ ๊ฒ์ด ์ข์ง๋ง ๋จ์ํ value ์ ์ฅ์ ์ํด ๋ง๋ ํด๋์ค์ด๋ฏ๋ก ๊ทธ๋ฅ ๋ถ์ฌ์ฃผ์๋ค.
๊ธฐ๋ณธ ์์ฑ์์ getter ๊ฐ์ ๊ฒฝ์ฐ๋ Jackson ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ JSON์ Java ๊ฐ์ฒด๋ก ๋ณํ์ ํ์ํ๊ธฐ ๋๋ฌธ์ ๋ถ์ฌ์ค์ผํ๋ค.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class SearchLog {
private String name;
private String createdAt;
}
SearchLogService ํด๋์ค์ ๊ฒ์ ๊ธฐ๋ก ์ ์ฅ, ์กฐํ, ์ญ์ ๋ก์ง์ ๊ตฌํํด์ฃผ์๋ค.
๋จผ์ ์ ์ฅ ๋ก์ง๋ถํฐ ์ดํด๋ณด์.
public void saveRecentSearchLog(Long memberId, SearchLogSaveRequest request) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String now = LocalDateTime.now().toString();
String key = "SearchLog" + member.getId();
SearchLog value = SearchLog.builder()
.name(request.getName())
.createdAt(now)
.build();
Long size = redisTemplate.opsForList().size(key);
if (size == 10) {
// rightPop์ ํตํด ๊ฐ์ฅ ์ค๋๋ ๋ฐ์ดํฐ ์ญ์
redisTemplate.opsForList().rightPop(key);
}
redisTemplate.opsForList().leftPush(key, value);
}
key๋ก ํ์ฌ ๋ก๊ทธ์ธํ SearchLog + [ํ์ฌ ๋ก๊ทธ์ธํ member์ id ๊ฐ] ์ผ๋ก ๋์๊ณ
value ๋ก SearchLog class ๋ก ๋์๋ค.
๋ง์ฝ redis์ ํ์ฌ ํฌ๊ธฐ๊ฐ 10์ธ ๊ฒฝ์ฐ rightTop์ ํตํด ๊ฐ์ฅ ์ค๋๋ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํด์ค๋ค.
10 ๋ฏธ๋ง์ด๋ผ๋ฉด leftPush๋ฅผ ํตํด ์๋ก์ด ๊ฒ์์ด๋ฅผ ์ถ๊ฐํด์ค๋ค.
๋ค์์ ์ต๊ทผ ๊ฒ์ ๊ธฐ๋ก ์กฐํ ๋ก์ง์ด๋ค.
public List<SearchLog> findRecentSearchLogs(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String key = "SearchLog" + member.getId();
List<SearchLog> logs = redisTemplate.opsForList().
range(key, 0, 10);
return logs;
}
opsForList().range() ํจ์๋ก Redis ๋ฆฌ์คํธ์์ key์ ํด๋นํ๋ ๊ฐ๋ค ์ค ์ธ๋ฑ์ค 0๋ถํฐ 10๊น์ง์ ๊ฐ์ ๊ฐ์ ธ์จ ํ ๋ฐํํด์ค๋ค.
๋ค์์ ๊ฒ์ ๊ธฐ๋ก ์ญ์ ๋ก์ง์ด๋ค.
public void deleteRecentSearchLog(Long memberId, SearchLogDeleteRequest request) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String key = "SearchLog" + member.getId();
SearchLog value = SearchLog.builder()
.name(request.getName())
.createdAt(request.getCreatedAt())
.build();
long count = redisTemplate.opsForList().remove(key, 1, value);
if (count == 0) {
throw new CustomException(ErrorCode.SEARCH_LOG_NOT_EXIST);
}
}
opsForList().remove() ํจ์๋ก ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํด์ฃผ๊ณ , ๋ฐํ๊ฐ์ผ๋ก ์ญ์ ๋ ํ์ ๊ฐ์๋ฅผ ๋ฐํํด์ค๋ค.
๊ฐ๋จํ๊ฒ TestController๋ฅผ ๋ง๋ค์ด ํ ์คํธ ํด๋ณด์๋ค.
@RestController
@RequiredArgsConstructor
@Slf4j
public class SearchLogController {
private final SearchLogService searchLogService;
@Operation(summary = "์ต๊ทผ ๊ฒ์ ๊ธฐ๋ก: ์ ์ฅ")
@PostMapping("/api/searchLog")
public ResponseEntity<Response> saveRecentSearchLog(@AuthenticationPrincipal PrincipalDetails principalDetails,
@RequestBody SearchLogSaveRequest request) {
searchLogService.saveRecentSearchLog(principalDetails.getMember().getId(), request);
return ResponseEntity.ok(new Response("์ต๊ทผ ๊ฒ์ ๊ธฐ๋ก ์ ์ฅ ์๋ฃ"));
}
@Operation(summary = "์ต๊ทผ ๊ฒ์ ๊ธฐ๋ก: ์กฐํ")
@GetMapping("/api/searchLogs")
public ResponseEntity<Response> findRecentSearchLog(@AuthenticationPrincipal PrincipalDetails principalDetails) {
List<SearchLog> recentSearchLogList = searchLogService.findRecentSearchLogs(principalDetails.getMember().getId());
return ResponseEntity.ok(new Response(recentSearchLogList, "์ต๊ทผ ๊ฒ์ ๊ธฐ๋ก ์กฐํ ์๋ฃ"));
}
}
redis-cli ์์ ํ์ธํด๋ณด๋ฉด ์ ์กฐํ๋๊ณ 10๊ฐ์ผ ๊ฒฝ์ฐ ๊ฐ์ฅ ์ค๋๋ ๊ฒ์ ๊ธฐ๋ก์ ์ง์ฐ๊ณ ์ ๊ฒ์ ๊ธฐ๋ก์ด insert ๋๋๊ฑธ ํ์ธํ ์ ์์๋ค.
swagger์์ ํ ์คํธ ์ ์๋ ์ฌ์ง๊ณผ ๊ฐ์ด ์ ์๋ต์ด ๋์๊ณ , Redis ๋ฅผ ์ฌ์ฉํ๊ธฐ์ ๋ณ๋์ ๋ก๊ทธ ๊ด๋ จ ๊ฒ์ ์ฟผ๋ฆฌ๋ ๋๊ฐ์ง ์์๋ค.
SearchLogService ์ ์ฒด ์ฝ๋
@Service
@RequiredArgsConstructor
public class SearchLogService {
private final RedisTemplate<String, SearchLog> redisTemplate;
private final MemberRepository memberRepository;
public void saveRecentSearchLog(Long memberId, SearchLogSaveRequest request) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String now = LocalDateTime.now().toString();
String key = "SearchLog" + member.getId();
SearchLog value = SearchLog.builder()
.name(request.getName())
.createdAt(now)
.build();
Long size = redisTemplate.opsForList().size(key);
if (size == 10) {
// rightPop์ ํตํด ๊ฐ์ฅ ์ค๋๋ ๋ฐ์ดํฐ ์ญ์
redisTemplate.opsForList().rightPop(key);
}
redisTemplate.opsForList().leftPush(key, value);
}
public List<SearchLog> findRecentSearchLogs(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String key = "SearchLog" + member.getId();
List<SearchLog> logs = redisTemplate.opsForList().
range(key, 0, 10);
return logs;
}
public void deleteRecentSearchLog(Long memberId, SearchLogDeleteRequest request) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String key = "SearchLog" + member.getId();
SearchLog value = SearchLog.builder()
.name(request.getName())
.createdAt(request.getCreatedAt())
.build();
long count = redisTemplate.opsForList().remove(key, 1, value);
if (count == 0) {
throw new CustomException(ErrorCode.SEARCH_LOG_NOT_EXIST);
}
}
}