스프링캐시 (1)

💻 Programming/Java

[5분코딩] Spring Boot Cache with EHCACHE

안녕하세요~ 오랜만에 포스팅을 하게되네요.

오늘은 스프링부트 기반의 프로젝트에서 스프링캐시를 사용하는 방법에 대해서 공유드리려고 합니다.

스프링캐시는 레디스, 카페인, ehcache 등과 연계하여 사용하게 되는데요,

이번  포스팅에서는 ehcache와 연동하여 사용할 예정입니다.

우선 기본적인 개발환경은 아래와 같습니다.

  • IntelliJ IDEA 2021.3.3 (Ultimate Edition)
  • SpringBoot 2.7.3
  • Java 18 (OpenJDK)

1. 프로젝트 생성

우선 빈 깡통 프로젝트를 만들어 볼게요.

이미 사용할 프로젝트가 있다면 건너뛰셔도 좋습니다 :)

자바 18, 그래들 선택
아무런 의존성을 선택하지 않고 깨끗한 프로젝트로 만듭니다
프로젝트 빌드가 완료된 후 프로젝트 트리

자, 빈깡통 프로젝트 생성이 완료되었습니다.

2. 라이브러리 추가 

이제 구현 및 테스트에 필요한 라이브러리들을 추가해볼게요.

우선 현재 build.gradle 파일은 아래와 같이 되어있을거에요

plugins {
    id 'org.springframework.boot' version '2.7.3'
    id 'io.spring.dependency-management' version '1.0.13.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '18'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

저기에 아래 5개 라이브러리들을 추가해줍니다.

implementation 'org.ehcache:ehcache:3.10.1'
implementation 'org.springframework:spring-context-support:5.3.22'

implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'

implementation 'ch.qos.logback:logback-classic:1.2.11'

왜 저 라이브러리들이 필요한지 하나씩 볼게요.

implementation 'org.ehcache:ehcache:3.10.1' jcache 구현체인 ehcache 사용을 위함
implementation 'org.springframework:spring-context-support:5.3.22' 스프링캐시의 어노테이션을 사용하기 위함
implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
lombok 어노테이션을 사용하기 위함
implementation 'ch.qos.logback:logback-classic:1.2.11' 로깅을 위함

이렇게 라이브러리를 추가한 뒤 캐시설정을 좀 해주도록 할게요

3. 캐시 설정

com.example.demo.config 패키지를 만든 뒤 아래와 같이 CacheConfig 클래스를 작성해줍니다.

package com.example.demo.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.cache.Cache;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;
import java.util.concurrent.TimeUnit;

@EnableCaching
@Configuration
public class CacheConfig {

    private final CachingProvider cachingProvider = Caching.getCachingProvider();
    private final javax.cache.CacheManager cacheManager = cachingProvider.getCacheManager();

    @Bean
    public CacheManager cacheManager() {
        return new JCacheCacheManager(cacheManager);
    }

    @Bean
    public Cache<Integer, Integer> commonCache() {
        MutableConfiguration<Integer, Integer> configuration =
                new MutableConfiguration<Integer, Integer>()
                        .setTypes(Integer.class, Integer.class)
                        .setStoreByValue(false)
                        .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, 5)));
        return cacheManager.createCache("commonCache", configuration);
    }
}

위 내용을 간략히 살펴보면 cacheManager 빈을 하나 등록해주고 그 캐시매니저에 commonCache라는 이름의 캐시 빈을 하나 등록해주는 겁니다. 이 commonCache가 key, value 페어를 저장하는 하나의 서랍이라고 생각하시면되고요. 여기서는 키와 값의 타입이 모두 Integer 이고 만료정책은 생성시간을 기준으로 5초로 설정해주었습니다.

 

4. API 구현 및 테스트

이제 commonCache에 값을 저장하고 만료되기 전까지 캐시의 데이터를 사용하도록 서비스 클래스를 하나 만들어주겠습니다.

package com.example.demo.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class CacheService {

    @Cacheable(cacheNames = "commonCache", cacheManager = "cacheManager", key = "#code")
    public Integer getCachedValue(int code) {
        System.out.println("계산중....");
        // DB 조회 등의 로직...
        return (int) (Math.random() * 10);
    }
}

com.example.demo.service 패키지를 만들고 CacheService 클래스를 작성했습니다.

getCachedValue 메서드에 @Cacheable 어노테이션을 붙여주고 commonCache를 사용하고 cacheManager는 CacheConfig에 등록했던 cacheManager 이름을 적어주었습니다. 그리고 key로 파라미터로 전달받는 code 값을 사용하겠다고 선언해주었습니다. 만약 key로 어떤 객체의 멤버변수를 사용해야한다면 key = "#obj.employeeId" 와 같이 사용가능합니다. (공식문서 참고)

CacheService.getCachedValue 가 호출되면 code 값을 key로하여 return된 Integer 값을 commonCache에 저장하게되고 만료시간인 5초 이내에 동일한 key값(여기서는 code 파라미터 값)으로 메서드 호출이 발생할 경우 메서드 내부의 비즈니스 로직을 타지않고 바로 캐시에 저장되어있는 값을 반환합니다. 이를 확인하기위해 계산중.... 메시지를 출력하도록 했습니다.

 

마지막으로 위 서비스를 호출하는 테스트 API 를 만들어 줄게요.

com.example.demo.controller 패키지를 신규추가하고 TestController를 생성하겠습니다.

package com.example.demo.controller;

import com.example.demo.service.CacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {

    private final CacheService cacheService;

    @GetMapping("/cache")
    public Integer getCachedValue(int code) {
        log.info("Request accepted for code {}", code);
        return cacheService.getCachedValue(code);
    }
}

API 호출이 들어올때마다 Request accepted for code X 로그가 출력되도록 하고, 캐시서비스의 메서드를 호출하도록 했습니다.

이제 DemoApplication을 실행하고 API를 호출해볼게요.

2022-09-14 12:32:59.783  INFO 47720 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.778 seconds (JVM running for 0.997)
2022-09-14 12:33:06.871  INFO 47720 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-09-14 12:33:06.871  INFO 47720 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-09-14 12:33:06.872  INFO 47720 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2022-09-14 12:33:06.882  INFO 47720 --- [nio-8080-exec-1] c.e.demo.controller.TestController       : Request accepted for code 1
계산중....
2022-09-14 12:33:08.270  INFO 47720 --- [nio-8080-exec-2] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:09.186  INFO 47720 --- [nio-8080-exec-3] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:10.017  INFO 47720 --- [nio-8080-exec-4] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:10.880  INFO 47720 --- [nio-8080-exec-5] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:11.764  INFO 47720 --- [nio-8080-exec-6] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:12.631  INFO 47720 --- [nio-8080-exec-7] c.e.demo.controller.TestController       : Request accepted for code 1
계산중....
2022-09-14 12:33:13.538  INFO 47720 --- [nio-8080-exec-8] c.e.demo.controller.TestController       : Request accepted for code 1
2022-09-14 12:33:14.408  INFO 47720 --- [nio-8080-exec-9] c.e.demo.controller.TestController       : Request accepted for code 1

호출은 모두 code값 1을 넣어서 하였고, 최초 호출을 12:33:06.882 에 하면서 서비스 내부로직을 실행하여 값을 반환하였습니다. (계산중.... 출력으로 확인)

이후 12:33:11.764 까지는 계산중.... 이 출력되지 않은 것으로 보아 캐싱되어있는 값을 그대로 응답한걸로 볼 수 있습니다.

5초가 지난 12:33:12.631 에 들어온 호출건은 계산중.... 이 출력된걸로 보아 응답값에 변동이 발생했을 것으로 추측됩니다. (랜덤값으로 응답하는 서비스니 응답값에 변동이 없을 수도 있죠)

여기서 중요한 것은 응답값에 변동이 있느냐가 아니라 캐시되어있는 데이터가 만료되기 전까지 메서드 내부로직을 실행하지 않고 캐시에서 조회된 데이터를 리턴해준다는 점입니다.

 

이상으로 5분만에 스프링캐시와 ehcache를 이용하여 캐시를 구현하는 방법에 대해 알아보았습니다.

 

읽어주셔서 감사합니다~  🙇