springboot (3)

💻 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를 이용하여 캐시를 구현하는 방법에 대해 알아보았습니다.

 

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

💻 Programming

springboot 2.4 업그레이드 시 gradle 버전 오류

An exception occurred applying plugin request [id: 'org.springframework.boot', version: '2.4.0']
> Failed to apply plugin [id 'org.springframework.boot']
   > Spring Boot plugin requires Gradle 5 (5.6.x only) or Gradle 6 (6.3 or later). The current version is Gradle 6.1.1

 

인텔리J에서 신규 그래들 프로젝트를 생성하고 스프링부트 최신버전인 2.4를 플러그인으로 추가했더니 위와같이 오류가 발생했다.

현재 gradle 6.1.1 버전을 사용중이고 스프링부트 플러그인을 사용하려면 6.3 이상의 버전이 필요하다는 거였다.

 

그래들은 최신버전이 현재 6.7.1 (그래들 공식사이트)이며 업그레이드는 아래 명령어를 실행하면 된다.

(주의: 우선 빌드시 에러가 발생하니 추가했던 내용을 주석처리 한뒤 실행한다. 또한 신규 프로젝트가 아닌 기존 프로젝트에서 그래들 버전을 업그레이드 할 시에는 공식문서를 충분히 읽어보고 진행할 것을 추천한다.)

> gradle wrapper --gradle-version 6.7.1

 

07:40:20: Executing tasks 'wrapper --gradle-version 6.7.1'...

> Task :wrapper

BUILD SUCCESSFUL in 344ms
1 actionable task: 1 executed
07:40:20: Tasks execution finished 'wrapper --gradle-version 6.7.1'.

이제 다시 build.gradle 파일에서 스프링부트 플러그인 주석을 해제하고 그래들 SYNC를 하면 아래처럼 6.7.1 버전을 다운로드하여 빌드에 성공한다.

Download https://services.gradle.org/distributions/gradle-6.7.1-bin.zip (102.84 MB)
Download https://services.gradle.org/distributions/gradle-6.7.1-bin.zip finished succeeded, took 12 s 837 ms
Starting Gradle Daemon...
Gradle Daemon started in 1 s 447 ms
> Task :prepareKotlinBuildScriptModel UP-TO-DATE

BUILD SUCCESSFUL in 1m 14s

 

 

 

 

 

참고문서: docs.gradle.org/current/userguide/upgrading_version_6.html

 

Upgrading your build from Gradle 6.x to the latest

This chapter provides the information you need to migrate your Gradle 6.x builds to the latest Gradle release. For migrating from Gradle 4.x or 5.x, see the older migration guide first. We recommend the following steps for all users: Try running gradle hel

docs.gradle.org

 

💻 Programming

스프링부트 스케쥴러 사용하기

스프링부트 스케쥴러

이번 포스팅에서는 스프링부트 프로젝트에서 스케쥴링 작업을 등록하여 사용하는 방법에 대해서 설명합니다.

스프링 부트에서 스케쥴러를 사용하려면 우선 아래처럼

@EnableScheduling 어노테이션을 @Configuration이 붙은 클래스에 등록해줘야 합니다.

 

스케쥴링 활성화를 위한 @EnableScheduling 사용

 

위 처럼 어노테이션을 붙여주면

스프링이 관리하는 빈 중에서 아래와 같이

@Scheduled 어노테이션이 붙어있는 것들을

찾아서 활성화 시켜주는 역할을 하게됩니다.

 

위 처럼 @Scheduled 어노테이션을 붙여주고 실행주기를 특정지어주면

해당 애플리케이션이 실행되면 자동으로 실행이 됩니다.

 

실행주기를 설정하는 방법은 기본적으로 세 가지가 있습니다.

 

fixedRate : 매 특정 밀리초마다 동작

fixedDelay : 해당 기능이 종료된 후 특정 밀리초 이후에 다시 동작

cron : 크론표현식에 정해진 내용대로 동작

 

여러 개의 스케쥴 잡을 등록할 때에는 주의할 점이 있습니다.

위 처럼 두 개의 스케쥴 잡이 등록되어있을 경우 한 번에 하나의 잡만 동작한다는 점입니다.

즉, @Scheduled가 붙은 기능들이 실행되는 시간이 중복될 경우 한 번에 하나의 작업만 실행됩니다.

 

만약 두 개 이상의 기능이 한꺼번에 실행되어도 성능에 문제가 없을 경우에는

아래처럼 @Async처리를 할 수 있습니다.

 

@Async 처리한 스케쥴 tasks

 

물론, @Async 가 적용되려면 @Configuration에 @EnableAsync도 추가해줘야 합니다.

 

여기까지 설정이 되었다면 위 두 scheduled task들은 동시에 실행됩니다.

 

추가로 @Async를 붙이지 않은 task를 하나 더 추가하면 어떻게 동작할까요?

task를 하나 더 추가해서 꼭!! 테스트해보세요.

 

이상으로 스프링부트에서 스케쥴링 잡을 등록하여 활용하는 방법에 대해 간단히 포스팅 해보았습니다.

유용했다면 좋아요 꾹!꾹!꾹! 눌러주세요 ^-^