[Redis] Redis๋กœ ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ๊ด€๋ฆฌํ•˜๊ธฐ

 

ํ”„๋กœ์ ํŠธ ์ค‘ ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•  ์ผ์ด ์ƒ๊ฒผ๋‹ค. ์ฒ˜์Œ์—” 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);
        }
    }
}