[Redis] 캐시를 통해 읽기 성능 향상하기 (2)

2025. 1. 3. 12:03·Data Infra/Redis

0. 들어가기 전

이전 글에서는 캐시의 정의와 다양한 캐시 전략에 대해 다뤘습니다.
자세한 내용이 궁금하신 분들은 아래 링크를 참고해 주세요.

 

https://young-code.tistory.com/39#2.%20캐싱%20전략%20패턴%20종류-1

 

[Redis] 캐시를 통해 읽기 성능 향상하기 (1)

1. 캐싱캐시 전략은 웹 서비스 환경에서 시스템 성능 향상을 기대할 수 있는 중요한 기술입니다. 일반적으로 캐시는 메모리를 사용하기 때문에 데이터베이스보다 훨씬 빠르게 데이터를 응답할

young-code.tistory.com

 

이번 글에서는 제가 프로젝트에서 캐시를 적용한 방법을 공유하려고 합니다.

 

1. 캐싱할 데이터

캐시는 조회는 빈번하지만 쓰기는 자주 발생하지 않는 데이터를 저장할 때 가장 효율적입니다. 성능 테스트를 진행하던 중, 팔로워 수가 많아질수록 SNS 메인 페이지 로딩 속도가 느려지고, 팔로우 관련 API 응답 시간이 지연되는 병목 현상을 발견했습니다. 특히 팔로우 데이터는 SNS 메인 페이지에서 게시물을 가져올 때 기준으로 활용되므로 조회 빈도가 매우 높습니다.

 

또한, 매번 서브쿼리를 실행하여 팔로우 데이터를 조회하는 방식이 성능 저하의 원인이었습니다. 이를 해결하기 위해 팔로우 데이터를 캐싱하여 서브쿼리를 제거하고, 캐싱된 데이터를 활용하는 방식으로 전환하는 것이 효과적이라고 판단했습니다.

아래는 팔로우 코드입니다.

@Entity
@Getter
@NoArgsConstructor
public class Follow extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "follower_id")
    private User follower;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "following_id")
    private User following;

    @Builder
    public Follow(User follower, User following) {
        this.follower = follower;
        this.following = following;
    }
}

 

2. 캐싱 전략 

팔로우 데이터는 캐싱된 데이터에 비해 상대적으로 쓰기 작업이 많다고 판단했습니다. 따라서, 쓰기 작업이 발생할 때마다 매번 DB를 거치는 Write-Through 방식보다는, Redis에서 데이터를 먼저 수정한 후 일정 주기로 배치 작업을 통해 DB와 정합성을 맞추는 Write-Back 전략이 더 효율적이라고 생각했습니다. 또한, 읽기 성능을 최적화하기 위해 Write-Back 전략과 궁합이 잘 맞는 Read-Through 방식을 적용하여, 요청이 있을 때 Redis에서 데이터를 조회하고, 캐시에 없는 경우 DB에서 가져와 Redis에 저장하는 방식을 선택했습니다.

 

🤔 Write-Back과 Read-Through 전략이 잘 어울리는 이유는 뭐지?

Write-Back 전략과 Read-Through 방식이 잘 맞는 이유는 캐시와 DB 간의 데이터 흐름과 일관성 유지 방식이 서로 보완적이기 때문입니다.

1. Write-Back의 특성
- 데이터를 변경할 때 즉시 DB에 반영하지 않고 Redis에서 먼저 수정한 후, 일정 주기로 배치 작업을 통해 DB에 동기화합니다.
- 즉, DB 부하를 줄이면서도 빠른 쓰기 성능을 확보할 수 있습니다.
- 하지만 Redis에서 변경된 데이터가 DB에 바로 반영되지 않기 때문에 DB와의 정합성 문제가 발생할 수 있습니다.

2. Read-Through의 특성
- 데이터를 조회할 때 먼저 Redis에서 조회하고, 없을 경우 DB에서 가져와 Redis에 캐싱합니다.
- 이를 통해 자주 조회되는 데이터의 성능을 향상시킬 수 있습니다.
- Redis에서 최신 데이터를 유지하는 것이 중요하기 때문에, Write-Back 방식과 함께 사용할 경우 데이터가 최신 상태로 유지될 가능성이 높아집니다.

3. 둘의 조합이 좋은 이유
- 쓰기 성능 최적화 : Write-Back은 자주 변경되는 데이터를 Redis에서 즉시 수정하여 DB 부하를 줄이고 빠른 응답 속도를 제공합니다.
- 일관성 유지 : Read-Through는 데이터를 읽을 때 Redis에서 조회하기 때문에, Write-Back이 적용된 데이터가 최신 상태로 유지되는 경우 캐시 적중률이 높아지고 DB 부하를 줄일 수 있습니다.
- 배치 업데이트의 장점 극대화 : Write-Back이 배치 업데이트를 수행할 때, Read-Through는 이전 업데이트된 Redis 데이터를 사용하여 최신 데이터를 제공하므로, 전체적인 성능과 정합성이 향상됩니다.

 

아래는 Write-Back 전략을 활용해서 구현한 코드입니다.

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FollowService {

    private final FollowRepository followRepository;
    private final UserRepository userRepository;
    private final StringRedisTemplate redisTemplate;

    private static final String FOLLOWING_KEY = "user:{userId}:following";
    private static final String FOLLOWER_KEY = "user:{userId}:follower";

    @Transactional
    public void follow(Long userId, Long targetUserId) {

        if (userId.equals(targetUserId)) {
            throw new ResourceNotFoundException(ErrorCode.CANNOT_FOLLOW_YOURSELF);
        }

        // 유저가 존재하는지 확인 (DB에서 조회)
        validateUsersExistence(userId, targetUserId);

        // 팔로잉 및 팔로워 목록 캐시 조회 및 저장
        Set<String> followingIds = getAndCacheFollowingIds(userId);
        Set<String> followerIds = getAndCacheFollowerIds(targetUserId);

        // 팔로우 관계를 Redis에 추가
        addFollowToRedis(userId, targetUserId);
    }

    // 유저가 존재하는지 확인 (DB에서 조회)
    private void validateUsersExistence(Long userId, Long targetUserId) {
        userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND));
        userRepository.findById(targetUserId)
                .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND));
    }

    public Set<String> getAndCacheFollowingIds(Long userId) {
        SetOperations<String, String> setOps = redisTemplate.opsForSet();
        Set<String> followingIds = setOps.members(FOLLOWING_KEY.replace("{userId}", userId.toString()));

        // 팔로잉 캐시에 데이터가 없으면 DB에서 팔로잉 목록을 조회
        if (followingIds == null || followingIds.isEmpty()) {
            List<Follow> followings = followRepository.findByFollowerId(userId);
            followingIds = followings.stream()
                    .map(follow -> follow.getFollowing().getId().toString())
                    .collect(Collectors.toSet());

            // 캐시에 DB에서 조회한 팔로잉 목록을 저장
            if (!followingIds.isEmpty()) {
                setOps.add(FOLLOWING_KEY.replace("{userId}", userId.toString()), followingIds.toArray(new String[0]));
                redisTemplate.expire(FOLLOWING_KEY.replace("{userId}", userId.toString()), 25, TimeUnit.HOURS);
            }
        }
        return followingIds;
    }

    public Set<String> getAndCacheFollowerIds(Long targetUserId) {
        SetOperations<String, String> setOps = redisTemplate.opsForSet();
        Set<String> followerIds = setOps.members(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()));

        // 팔로워 캐시에 데이터가 없으면 DB에서 팔로워 목록을 조회
        if (followerIds == null || followerIds.isEmpty()) {
            List<Follow> followers = followRepository.findByFollowingId(targetUserId);
            followerIds = followers.stream()
                    .map(follow -> follow.getFollower().getId().toString())
                    .collect(Collectors.toSet());

            // 캐시에 DB에서 조회한 팔로워 목록을 저장
            if (!followerIds.isEmpty()) {
                setOps.add(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()), followerIds.toArray(new String[0]));
                redisTemplate.expire(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()), 25, TimeUnit.HOURS);
            }
        }
        return followerIds;
    }

    private void addFollowToRedis(Long userId, Long targetUserId) {
        SetOperations<String, String> setOps = redisTemplate.opsForSet();
        setOps.add(FOLLOWING_KEY.replace("{userId}", userId.toString()), String.valueOf(targetUserId));
        setOps.add(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()), String.valueOf(userId));
    }

    @Transactional
    public void unfollow(Long userId, Long targetUserId) {

        // 유저가 존재하는지 확인
        validateUsersExistence(userId, targetUserId);

        // 팔로잉 및 팔로워 목록 캐시 조회 및 저장
        Set<String> followingIds = getAndCacheFollowingIds(userId);
        Set<String> followerIds = getAndCacheFollowerIds(targetUserId);

        // Redis에서 팔로잉 목록에서 제거
        String followingKey = FOLLOWING_KEY.replace("{userId}", userId.toString());
        String followerKey = FOLLOWER_KEY.replace("{userId}", targetUserId.toString());

        SetOperations<String, String> setOps = redisTemplate.opsForSet();
        setOps.remove(followingKey, String.valueOf(targetUserId));  // 팔로잉 목록에서 제거
        setOps.remove(followerKey, String.valueOf(userId));  // 팔로워 목록에서 제거
    }

    public List<FollowResponseDTO> getFollowings(Long userId) {
        Set<String> followingIds = getAndCacheFollowingIds(userId);

        // 팔로잉 목록을 Redis에서 가져왔으면 DTO로 변환하여 반환
        return followingIds.stream()
                .map(followingId -> getUser(Long.valueOf(followingId)))  // true는 팔로잉 여부
                .map(FollowResponseDTO::from)
                .collect(Collectors.toList());
    }

    public List<FollowResponseDTO> getFollowers(Long userId) {
        // Redis에서 팔로잉 목록을 가져오고 캐시가 없다면 DB에서 조회
        Set<String> followerIds = getAndCacheFollowerIds(userId);

        // 팔로잉 목록을 Redis에서 가져왔으면 DTO로 변환하여 반환
        return followerIds.stream()
                .map(followerId -> getUser(Long.valueOf(followerId)))  // true는 팔로잉 여부
                .map(FollowResponseDTO::from)
                .collect(Collectors.toList());
    }

    private User getUser(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND));
    }

    public boolean isFollowing(Long userId, Long targetUserId) {
        SetOperations<String, String> setOps = redisTemplate.opsForSet();
        return Boolean.TRUE.equals(setOps.isMember(FOLLOWING_KEY.replace("{userId}", userId.toString()), String.valueOf(targetUserId)));
    }

    public FollowCountDTO getFollowCount(Long userId) {
        // Redis에서 팔로잉 수를 가져옴
        Long followingCount = redisTemplate.opsForSet().size(FOLLOWING_KEY);
        // Redis에서 팔로워 수를 가져옴
        Long followerCount = redisTemplate.opsForSet().size(FOLLOWER_KEY);

        // 팔로잉 수가 Redis에서 0이거나 null인 경우 DB에서 가져옴
        if (followingCount == null || followingCount == 0) {
            followingCount = followRepository.countByFollowerId(userId);
        }

        // 팔로워 수가 Redis에서 0이거나 null인 경우 DB에서 가져옴
        if (followerCount == null || followerCount == 0) {
            followerCount = followRepository.countByFollowingId(userId);
        }

        return new FollowCountDTO(followingCount != null ? followingCount : 0, followerCount != null ? followerCount : 0);
    }
}

 

a. Method

 

a-1. 유저가 존재하는지 확인

private void validateUsersExistence(Long userId, Long targetUserId) {
    userRepository.findById(userId)
            .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND));
    userRepository.findById(targetUserId)
            .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND));
}

validateUsersExistence 메서드는 주어진 userId와 targetUserId가 존재하는지 확인하는 역할을 하는 메서드입니다.

먼저, userRepository.findById(userId)를 호출하여 해당 userId를 가진 사용자가 존재하는지 조회하며, 존재하지 않을 경우 USER_NOT_FOUND 오류 코드와 함께 ResourceNotFoundException 예외를 발생시킵니다. 같은 방식으로 targetUserId도 조회하며, 존재하지 않는 경우 동일한 예외를 던집니다. 이를 통해 두 사용자 ID가 모두 유효한 경우에만 이후의 로직이 실행될 수 있도록 보장하는 메서드입니다.

 

a-2. Redis에 데이터가 있는지 조회

public Set<String> getAndCacheFollowingIds(Long userId) {
    SetOperations<String, String> setOps = redisTemplate.opsForSet();
    Set<String> followingIds = setOps.members(FOLLOWING_KEY.replace("{userId}", userId.toString()));

    // 팔로잉 캐시에 데이터가 없으면 DB에서 팔로잉 목록을 조회
    if (followingIds == null || followingIds.isEmpty()) {
        List<Follow> followings = followRepository.findByFollowerId(userId);
        followingIds = followings.stream()
                .map(follow -> follow.getFollowing().getId().toString())
                .collect(Collectors.toSet());

        // 캐시에 DB에서 조회한 팔로잉 목록을 저장
        if (!followingIds.isEmpty()) {
            setOps.add(FOLLOWING_KEY.replace("{userId}", userId.toString()), followingIds.toArray(new String[0]));
            redisTemplate.expire(FOLLOWING_KEY.replace("{userId}", userId.toString()), 25, TimeUnit.HOURS);
        }
    }
    return followingIds;
}

public Set<String> getAndCacheFollowerIds(Long targetUserId) {
    SetOperations<String, String> setOps = redisTemplate.opsForSet();
    Set<String> followerIds = setOps.members(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()));

    // 팔로워 캐시에 데이터가 없으면 DB에서 팔로워 목록을 조회
    if (followerIds == null || followerIds.isEmpty()) {
        List<Follow> followers = followRepository.findByFollowingId(targetUserId);
        followerIds = followers.stream()
                .map(follow -> follow.getFollower().getId().toString())
                .collect(Collectors.toSet());

        // 캐시에 DB에서 조회한 팔로워 목록을 저장
        if (!followerIds.isEmpty()) {
            setOps.add(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()), followerIds.toArray(new String[0]));
            redisTemplate.expire(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()), 25, TimeUnit.HOURS);
        }
    }
    return followerIds;
}

getAndCacheFollowingIds 메서드는 특정 사용자의 팔로잉 ID 목록을 Redis에서 가져오며, 만약 캐시에 데이터가 없으면 DB에서 조회한 후 Redis에 저장하고 반환하는 역할을 합니다.

 

getAndCacheFollowerIds 메서드는 특정 사용자의 팔로워 ID 목록을 Redis에서 가져오며, 캐시에 데이터가 없을 경우 DB에서 조회한 후 Redis에 저장하고 반환하는 역할을 합니다.

 

 

b. follow

@Transactional
public void follow(Long userId, Long targetUserId) {

    if (userId.equals(targetUserId)) {
        throw new ResourceNotFoundException(ErrorCode.CANNOT_FOLLOW_YOURSELF);
    }

    // 유저가 존재하는지 확인 (DB에서 조회)
    validateUsersExistence(userId, targetUserId);

    // 팔로잉 및 팔로워 목록 캐시 조회 및 저장
    Set<String> followingIds = getAndCacheFollowingIds(userId);
    Set<String> followerIds = getAndCacheFollowerIds(targetUserId);

    // 팔로우 관계를 Redis에 추가
    addFollowToRedis(userId, targetUserId);
}

private void addFollowToRedis(Long userId, Long targetUserId) {
    SetOperations<String, String> setOps = redisTemplate.opsForSet();
    setOps.add(FOLLOWING_KEY.replace("{userId}", userId.toString()), String.valueOf(targetUserId));
    setOps.add(FOLLOWER_KEY.replace("{userId}", targetUserId.toString()), String.valueOf(userId));
}

follow 메서드는 한 사용자가 다른 사용자를 팔로우할 때 실행됩니다. 먼저, 자신을 팔로우하는 경우 예외를 발생시키고, 팔로우할 대상과 본인이 실제 존재하는 사용자인지 DB에서 확인합니다. 이후, 해당 사용자의 팔로잉 및 팔로워 목록을 Redis에서 가져오거나, 캐시에 없으면 DB에서 조회하여 저장합니다. 마지막으로, 팔로우 관계를 Redis에 추가하여 캐시를 최신 상태로 유지합니다.

 

addFollowToRedis 메서드는 Redis의 Set 자료구조를 사용해 팔로우 관계를 저장하는 역할을 합니다. 한 사용자의 팔로잉 목록과 상대방의 팔로워 목록을 각각 Redis에 추가합니다.

 

 

c. unfollow

@Transactional
public void unfollow(Long userId, Long targetUserId) {

    // 유저가 존재하는지 확인
    validateUsersExistence(userId, targetUserId);

    // 팔로잉 및 팔로워 목록 캐시 조회 및 저장
    Set<String> followingIds = getAndCacheFollowingIds(userId);
    Set<String> followerIds = getAndCacheFollowerIds(targetUserId);

    // Redis에서 팔로잉 목록에서 제거
    String followingKey = FOLLOWING_KEY.replace("{userId}", userId.toString());
    String followerKey = FOLLOWER_KEY.replace("{userId}", targetUserId.toString());

    SetOperations<String, String> setOps = redisTemplate.opsForSet();
    setOps.remove(followingKey, String.valueOf(targetUserId));  // 팔로잉 목록에서 제거
    setOps.remove(followerKey, String.valueOf(userId)); 
}

사용자가 다른 사용자를 언팔로우할 때 실행됩니다. 먼저, 두 사용자가 실제로 존재하는지 확인합니다. 그런 다음, 캐시에서 해당 사용자의 팔로잉 및 팔로워 목록을 가져오거나 저장합니다. 이후, Redis에서 팔로우 관계를 제거하는데, 특정 사용자의 팔로잉 목록에서 상대방의 ID를 삭제하고, 상대방의 팔로워 목록에서도 해당 사용자의 ID를 삭제하는 방식으로 언팔로우를 처리합니다.

 

 

d. follow 조회

public List<FollowResponseDTO> getFollowings(Long userId) {

    Set<String> followingIds = getAndCacheFollowingIds(userId);

    // 팔로잉 목록을 Redis에서 가져왔으면 DTO로 변환하여 반환
    return followingIds.stream()
            .map(followingId -> getUser(Long.valueOf(followingId)))  // true는 팔로잉 여부
            .map(FollowResponseDTO::from)
            .collect(Collectors.toList());
}

public List<FollowResponseDTO> getFollowers(Long userId) {

    // Redis에서 팔로잉 목록을 가져오고 캐시가 없다면 DB에서 조회
    Set<String> followerIds = getAndCacheFollowerIds(userId);

    // 팔로잉 목록을 Redis에서 가져왔으면 DTO로 변환하여 반환
    return followerIds.stream()
            .map(followerId -> getUser(Long.valueOf(followerId)))  // true는 팔로잉 여부
            .map(FollowResponseDTO::from)
            .collect(Collectors.toList());
}

private User getUser(Long userId) {
    return userRepository.findById(userId)
            .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND));
}

이 코드는 특정 사용자의 팔로잉 및 팔로워 목록을 조회하는 기능을 제공합니다. 먼저, 사용자의 팔로잉 ID 또는 팔로워 ID 목록을 Redis에서 가져오고, 만약 Redis에 데이터가 없다면 데이터베이스에서 조회한 후 Redis에 캐싱합니다. 이후, 조회된 ID 목록을 기반으로 실제 사용자 정보를 가져오고, 해당 정보를 FollowResponseDTO 객체로 변환하여 반환합니다. 사용자가 존재하지 않을 경우 예외를 발생시키는 로직도 포함되어 있습니다.

 

 

e. follow 상태 조회

public boolean isFollowing(Long userId, Long targetUserId) {
    SetOperations<String, String> setOps = redisTemplate.opsForSet();
    return Boolean.TRUE.equals(setOps.isMember(FOLLOWING_KEY.replace("{userId}", userId.toString()), String.valueOf(targetUserId)));
}

이 코드는 특정 사용자가 다른 사용자를 팔로우하고 있는지 확인하는 기능을 제공합니다. Redis의 SetOperations을 사용하여, 주어진 userId의 팔로잉 목록에서 targetUserId가 포함되어 있는지 검사합니다. Redis에서 해당 사용자가 팔로우 중이라면 true를 반환하고, 아니라면 false를 반환합니다.

 

 

f. follow 수 조회

public FollowCountDTO getFollowCount(Long userId) {

    // Redis에서 팔로잉 수를 가져옴
    Long followingCount = redisTemplate.opsForSet().size(FOLLOWING_KEY);
    // Redis에서 팔로워 수를 가져옴
    Long followerCount = redisTemplate.opsForSet().size(FOLLOWER_KEY);

    // 팔로잉 수가 Redis에서 0이거나 null인 경우 DB에서 가져옴
    if (followingCount == null || followingCount == 0) {
        followingCount = followRepository.countByFollowerId(userId);
    }

    // 팔로워 수가 Redis에서 0이거나 null인 경우 DB에서 가져옴
    if (followerCount == null || followerCount == 0) {
        followerCount = followRepository.countByFollowingId(userId);
    }

    return new FollowCountDTO(followingCount != null ? followingCount : 0, followerCount != null ? followerCount : 0);
}

이 코드는 특정 사용자의 팔로잉 수와 팔로워 수를 조회하는 기능을 제공합니다. 먼저, Redis에서 해당 사용자의 팔로잉 수와 팔로워 수를 가져옵니다. 만약 Redis에 저장된 값이 없거나(null) 0이면, DB에서 해당 사용자의 팔로잉 및 팔로워 수를 조회합니다. 이후 조회된 값을 FollowCountDTO 객체로 변환하여 반환합니다.

 

위와 같이 캐시를 진행하여 서브쿼리를 제거하고 캐싱된 데이터로 쿼리를 최적화활 수 있었습니다.

// 최적화 전
@Query("SELECT p FROM Post p " +
        "WHERE p.writer.id IN " +
        "(SELECT f.following.id FROM Follow f WHERE f.follower.id = :userId) " +
        "ORDER BY p.createdAt DESC")
Page<Post> findFollowingUsersPosts(@Param("userId") Long userId, Pageable pageable);

// 최적화 후
@Query("SELECT p FROM Post p " +
        "WHERE p.writer.id IN :followingIds " +
        "ORDER BY p.createdAt DESC")
Page<Post> findFollowingUsersPostsByIds(@Param("followIds") Set<String> followingIds, Pageable pageable);

 

 

3. 성능 비교

 

  • 캐싱 적용 전

  • 캐싱 적용 후

  • 비교

 

캐싱을 적용한 경우 Min 값에서는 큰 차이가 없지만, Max 값을 보면 25와 213으로 상당한 차이가 나타납니다. 이는 트래픽이 집중될 때, 인메모리 방식의 Redis 캐싱이 빠르게 데이터를 제공한다는 것을 보여줍니다.

 

💡 성능 테스트 조건

- User : 11
- Post : 5000
- Follow : 4000

- Number of Threads (users) : 100
- Ramp-up period (seconds) : 1
- Loop-Count : 10

'Data Infra > Redis' 카테고리의 다른 글

[Redis] 캐시를 통해 읽기 성능 향상하기 (1)  (0) 2024.12.23
'Data Infra/Redis' 카테고리의 다른 글
  • [Redis] 캐시를 통해 읽기 성능 향상하기 (1)
chanyoungdev
chanyoungdev
chanyoungdev
  • chanyoungdev
    Young Code
    chanyoungdev
  • 전체
    오늘
    어제
    • 분류 전체보기 (28)
      • Programming (12)
        • Java (0)
        • Spring (10)
        • etc (2)
      • Data Infra (5)
        • Database (1)
        • Redis (2)
        • etc (2)
      • DevOps (4)
        • CI CD (1)
        • Nginx (2)
        • Docker (0)
        • Aws (0)
        • etc (1)
      • Algorithm (6)
      • etc (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • First Tech Blog
    • Github
  • 공지사항

  • 인기 글

  • 태그

    cache
    공통 응답
    Spring Batch
    elasticsearch
    단위 테스트
    Infra
    infra architecture
    Algorithm
    lock
    junit
    JWT
    ssl
    nginx
    Virtual Thread
    spring security
    캐시
    Redis
    Mockito
    domain
    OAuth
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
chanyoungdev
[Redis] 캐시를 통해 읽기 성능 향상하기 (2)
상단으로

티스토리툴바