자바에서 캐시 사용하기: Caffeine 캐시 설정과 사용법

캐시는 애플리케이션 성능을 크게 향상시킬 수 있는 중요한 도구입니다. 자바에서 캐시를 사용하면 데이터베이스 조회나 복잡한 계산과 같은 비용이 많이 드는 작업의 결과를 임시 저장하여 빠르게 재사용할 수 있습니다. 이번 포스팅에서는 자바에서 사용 가능한 다양한 캐시 솔루션과 그 중에서도 JVM 내에서 사용하기 편리한 Caffeine 캐시에 대해 알아보겠습니다.
자바에서 캐시 사용하기: Caffeine 캐시 설정과 사용법
Photo by Mike Kenneally / Unsplash

On this page

캐시는 애플리케이션 성능을 크게 향상시킬 수 있는 중요한 도구입니다. 자바에서 캐시를 사용하면 데이터베이스 조회나 복잡한 계산과 같은 비용이 많이 드는 작업의 결과를 임시 저장하여 빠르게 재사용할 수 있습니다. 이번 포스팅에서는 자바에서 사용 가능한 다양한 캐시 솔루션과 그 중에서도 JVM 내에서 사용하기 편리한 Caffeine 캐시에 대해 알아보겠습니다.

자바에서 사용 가능한 캐시 종류

  1. Ehcache: 자바에서 가장 널리 사용되는 캐시 라이브러리 중 하나입니다. 복잡한 캐시 관리와 다양한 설정을 지원합니다.
  2. Guava Cache: Google의 Guava 라이브러리에 포함된 간단한 캐시 기능입니다. 기본적인 캐시 기능을 제공하며 사용이 쉽습니다.
  3. Caffeine: Guava Cache의 후속작으로, 높은 성능과 다양한 캐시 전략을 제공합니다. JVM 내에서 사용하기 편리하며 설정이 간단합니다.
  4. Redis: 메모리 기반의 NoSQL 데이터베이스로, 분산 캐시로 자주 사용됩니다. Spring Data Redis를 통해 쉽게 사용할 수 있습니다.
  5. Hazelcast: 분산 데이터 그리드를 제공하는 라이브러리로, 클러스터링된 환경에서 사용할 수 있습니다.

이번 포스팅에서는 Caffeine 캐시에 대해 집중적으로 다루겠습니다.

Caffeine 캐시 설정 및 사용법

Caffeine 캐시는 자바에서 고성능의 메모리 내 캐시를 제공하는 라이브러리입니다. 설정과 사용이 간단하면서도 다양한 캐시 전략을 지원합니다. 아래에서는 Caffeine 캐시를 설치하고, 설정하고, 사용하는 방법을 단계별로 설명하겠습니다.

1. Caffeine 캐시 의존성 추가

먼저 Gradle을 사용하여 프로젝트에 Caffeine 캐시 의존성을 추가합니다.

implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.github.ben-manes.caffeine:caffeine:3.1.2")

2. CacheConfig 클래스 설정

Caffeine 캐시를 설정하는 클래스를 작성합니다. 이 클래스에서는 캐시의 만료 시간과 최대 크기를 설정합니다.

package com.yourcompany.yourproject.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("dashboardSummary", "wordCloudCache", "barChartCache");
        cacheManager.setCaffeine(caffeineCacheBuilder());
        cacheManager.registerCustomCache("wordCloudCache", wordCloudCacheBuilder().build());
        cacheManager.registerCustomCache("barChartCache", barChartCacheBuilder().build());
        return cacheManager;
    }

    Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .expireAfterWrite(12, TimeUnit.HOURS)
                .maximumSize(100);
    }

    Caffeine<Object, Object> wordCloudCacheBuilder() {
        return Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .maximumSize(100);
    }

    Caffeine<Object, Object> barChartCacheBuilder() {
        return Caffeine.newBuilder()
                .expireAfterWrite(60, TimeUnit.MINUTES)
                .maximumSize(100);
    }
}

3. 서비스 클래스 설정

서비스 클래스에서 캐시를 사용하도록 설정합니다. 각 서비스 메서드에 대해 캐시를 설정하여 필요한 데이터를 캐싱합니다.

package com.yourcompany.yourproject.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
import com.yourcompany.yourproject.dto.DashboardSummaryGetDto;
import com.yourcompany.yourproject.dto.DashboardWordCloudGetDto;
import com.yourcompany.yourproject.dto.DashboardBarChartGetDto;
import com.yourcompany.yourproject.repository.CrawRepository;

@Service
public class DashboardService {

    @Autowired
    private CrawRepository crawRepository;

    @Cacheable("dashboardSummary")
    public List<DashboardSummaryGetDto> getSummary() {
        return crawRepository.selectSummary();
    }

    @CacheEvict(value = "dashboardSummary", allEntries = true)
    public List<DashboardSummaryGetDto> refreshCache() {
        List<DashboardSummaryGetDto> summary = crawRepository.selectSummary();
        return summary;
    }

    @Cacheable("wordCloudCache")
    public List<DashboardWordCloudGetDto> getWordCloud(DashboardWordCloudSetDto dashboardWordCloudSetDto) {
        log.info("dashboardWordCloudSetDto : {}", dashboardWordCloudSetDto);
        return crawRepository.selectWordCloud(dashboardWordCloudSetDto.getCountry());
    }

    @Cacheable("barChartCache")
    public List<DashboardBarChartGetDto> getBarChart() {
        return crawRepository.selectBarChart();
    }
}

4. 스케줄러 클래스 설정

캐시를 주기적으로 갱신하도록 스케줄러를 설정합니다. 예를 들어 매일 아침 7시 55분에 캐시를 갱신하도록 설정합니다.

package com.yourcompany.yourproject.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.yourcompany.yourproject.service.DashboardService;

@Component
public class CacheScheduler {

    @Autowired
    private DashboardService dashboardService;

    @Scheduled(cron = "0 55 7 * * ?")
    public void clearCache() {
        dashboardService.refreshCache();
    }
}

5. 애플리케이션 클래스 설정

Spring Boot 애플리케이션 클래스에 필요한 애너테이션을 추가합니다.

package com.yourcompany.yourproject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@SpringBootApplication
@EnableWebMvc
@EnableCaching
@EnableScheduling
public class ApiApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class, args);
    }
}

이렇게 설정하면 캐시는 12시간 동안 유지되며, 매일 아침 7시 55분에 자동으로 갱신됩니다. 또한, 다른 함수에 대해서도 각각 5분과 60분의 캐시 만료 시간을 설정할 수 있습니다. Redis와 같은 외부 캐시 솔루션을 설치하지 않아도 JVM 내에서 고성능 캐싱을 사용할 수 있습니다.

Caffeine 캐시는 간단하면서도 강력한 기능을 제공하므로, 자바 애플리케이션에서 캐시를 효과적으로 사용할 수 있습니다. 이 포스팅이 여러분의 프로젝트에 도움이 되길 바랍니다.


스케줄러 주기설정 및 성능

1. 스케줄러를 특정 주기로 설정

Spring의 @Scheduled 어노테이션을 사용하여 특정 주기로 작업을 실행할 수 있습니다. 예를 들어, 10분마다 캐시를 갱신하도록 설정하려면 fixedRatecron 속성을 사용하면 됩니다.

fixedRate 사용

fixedRate 속성을 사용하면 이전 작업이 끝난 후 일정 시간 간격으로 작업이 실행됩니다.

@Scheduled(fixedRate = 600000) // 10분 = 600,000 밀리초
public void clearCaches() {
    DashboardWordCloudSetDto wordCloudSetDto = new DashboardWordCloudSetDto(); // 필요한 데이터로 초기화
    dashboardService.refreshDashboardSummaryCache();
    dashboardService.refreshWordCloudCache(wordCloudSetDto);
    dashboardService.refreshBarChartCache();
}

cron 사용

cron 표현식을 사용하여 10분마다 작업이 실행되도록 설정할 수도 있습니다.

@Scheduled(cron = "0 */10 * * * ?") // 매 10분마다 실행
public void clearCaches() {
    DashboardWordCloudSetDto wordCloudSetDto = new DashboardWordCloudSetDto(); // 필요한 데이터로 초기화
    dashboardService.refreshDashboardSummaryCache();
    dashboardService.refreshWordCloudCache(wordCloudSetDto);
    dashboardService.refreshBarChartCache();
}

2. Caffeine 캐시의 성능

Caffeine 캐시는 높은 성능과 효율성을 제공하는 것으로 잘 알려져 있습니다. Caffeine의 주요 특징은 다음과 같습니다:

  • 높은 성능: Caffeine은 최신 캐시 알고리즘을 사용하여 높은 성능을 제공합니다.
  • 효율적인 메모리 사용: Caffeine은 메모리 사용을 최소화하면서도 높은 히트율을 제공합니다.
  • 스레드 안전성: Caffeine은 멀티스레드 환경에서 안전하게 동작합니다.

일반적으로, JVM 내에서 Caffeine을 사용하는 것은 성능 이슈를 일으키지 않습니다. 그러나 애플리케이션의 특정 요구 사항과 환경에 따라 다를 수 있으므로, 실제 사용 시 성능 테스트를 통해 검증하는 것이 좋습니다.


지연시간을 줄이기위한 1회 자동 호출

만약 매일 7시 55분에 캐시를 갱신하고 그 이후 처음 들어갈 때 발생하는 지연 시간을 줄이기 위해 외부에서 직접 호출하는 스케줄링을 추가하고 싶다면, Spring의 RestTemplate이나 WebClient를 사용하여 이를 구현할 수 있습니다. 이렇게 하면 캐시를 갱신한 후 특정 URL을 호출하여 캐시를 미리 로드할 수 있습니다.

스케줄러 클래스 수정

매일 7시 55분에 캐시를 갱신한 후 외부에서 해당 URL을 호출하도록 스케줄러를 설정합니다.

src/main/java/com/yourcompany/yourproject/config/CacheScheduler.java

package com.yourcompany.yourproject.config;

import com.yourcompany.yourproject.service.DashboardService;
import com.yourcompany.yourproject.dto.DashboardWordCloudSetDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class CacheScheduler {

    @Autowired
    private DashboardService dashboardService;

    private final RestTemplate restTemplate = new RestTemplate();
    private static final String CACHE_LOAD_URL = "<http://localhost:8080/loadCache>";

    @Scheduled(cron = "0 55 7 * * ?")
    public void clearCaches() {
        dashboardService.refreshDashboardSummaryCache();
        dashboardService.refreshWordCloudCache(new DashboardWordCloudSetDto());
        dashboardService.refreshBarChartCache();

        // 외부 URL 호출하여 캐시를 미리 로드
        restTemplate.getForObject(CACHE_LOAD_URL, String.class);
    }
}

캐시 로드를 위한 컨트롤러 작성

캐시를 로드하는 URL을 처리하는 컨트롤러를 작성합니다.

src/main/java/com/yourcompany/yourproject/controller/CacheController.java

package com.yourcompany.yourproject.controller;

import com.yourcompany.yourproject.service.DashboardService;
import com.yourcompany.yourproject.dto.DashboardWordCloudSetDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CacheController {

    @Autowired
    private DashboardService dashboardService;

    @GetMapping("/loadCache")
    public String loadCache() {
        dashboardService.getSummary();
        dashboardService.getWordCloud(new DashboardWordCloudSetDto());
        dashboardService.getBarChart();
        return "Cache loaded";
    }
}

이렇게 하면 매일 7시 55분에 캐시를 갱신하고, 외부 URL을 호출하여 캐시를 미리 로드할 수 있습니다. 이를 통해 첫 요청 시의 지연 시간을 줄일 수 있습니다.


키 기반의 캐싱 처리

Caffeine Cache를 사용하여 키 기반 캐싱을 처리할 수 있습니다. Caffeine Cache는 복잡한 키를 정의하고, 해당 키로 캐싱 결과를 저장하고 불러오는 기능을 제공합니다.

다음은 스프링 부트에서 Caffeine Cache를 사용하여 Python에서 Redis로 구현한 캐싱과 유사한 방식으로 캐싱을 설정하는 예제입니다.

1. 의존성 추가

build.gradle 파일에 Caffeine 의존성을 추가합니다.

implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.2'

2. CacheConfig 클래스 설정

캐시 설정 클래스를 작성합니다.

package com.example.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(caffeineCacheBuilder());
        return cacheManager;
    }

    Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .maximumSize(100);
    }
}

3. CacheKeyGenerator 클래스 작성

캐시 키를 생성하는 클래스입니다.

package com.example.util;

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.StringJoiner;

@Component("customKeyGenerator")
public class CacheKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringJoiner key = new StringJoiner(":");
        key.add(target.getClass().getSimpleName());
        key.add(method.getName());
        for (Object param : params) {
            key.add(param.toString());
        }
        return key.toString();
    }
}

4. 서비스 클래스에서 캐시 적용

캐싱을 적용할 서비스 클래스에서 캐시 키 생성기를 사용합니다.

package com.example.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ExampleService {

    @Cacheable(value = "exampleCache", keyGenerator = "customKeyGenerator")
    public String getData(String query, String orderOption, String displayOption, String appendYn) {
        // 실제 데이터 처리 로직
        return "Data from service for query: " + query;
    }
}

5. 캐시 사용 예시

컨트롤러 또는 다른 서비스에서 캐시를 사용하는 방법입니다.

package com.example.controller;

import com.example.service.ExampleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ExampleController {

    @Autowired
    private ExampleService exampleService;

    @GetMapping("/getData")
    public String getData(
            @RequestParam String query,
            @RequestParam String orderOption,
            @RequestParam String displayOption,
            @RequestParam String appendYn) {
        return exampleService.getData(query, orderOption, displayOption, appendYn);
    }
}

이렇게 설정하면 ExampleService.getData 메서드에 대한 호출이 특정 키를 기반으로 캐싱되고, 동일한 키로 요청이 들어올 경우 캐시된 결과를 반환합니다. 이를 통해 동일한 요청에 대해 캐싱을 적용하여 성능을 최적화할 수 있습니다.

Subscribe to Keun's Story newsletter and stay updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox. It's free!
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!