본문 바로가기
실습/리눅스 서버 + 스프링 부트

Spring Boot 캐시 사용

by 이민우 2022. 3. 11.
728x90
반응형

개발이 완료된 프로그램이 있다. 해당 프로그램은 필요 이상의 인프라 (DB, RabbitMQ 등)를 요구했고, 이 점으로 인해 설치가 어렵고 프로그램의 복잡도가 높았다.

 

그래서 해당 프로그램의 인프라 의존도를 낮추기 위한 수정 작업을 수행했다. 그 중 하나는 Redis에 저장하는 정보를 캐시에 저장하여 Redis에 대한 의존성을 없애는 작업이었다.

 

아래는 해당 작업을 위해 공부한 내용이다.

 

 

 

Cache

 

캐시는 데이터의 값을 복사해놓는 임시 저장소이다.

 

주 메모리에 비해 데이터에 접근하는 시간이 짧기 때문에, 자주 사용하는 정보를 임시로 저장해놓고 사용한다.

 

스프링에서 Cache는 아래의 어노테이션을 사용하면 쉽게 사용할 수 있다.

 

  • @Cacheable
  • @CachePut
  • @CacheEvict

 

 

 

 

데이터 준비

 

실습을 위해 간단한 테이블을 만들었다.

 

그리고 해당 테이블에 프로시져를 사용하여 10,000개의 데이터를 넣어주었다.

DELIMITER $$
CREATE PROCEDURE insertUntil(max INT)
BEGIN
	DECLARE i INT DEFAULT 1;
	WHILE (i <= max) DO
		INSERT INTO test.test_table VALUE (i, i, i, i, i, i, i, i, i, i, i);
		SET i = i+1;
	END WHILE;
END$$
DELIMITER ;
CALL insertUntil(10000);

 

 

 

프로젝트 생성

 

이제 프로젝트를 만들어주자. 설정 사항은 아래와 같다.

 

 

그럼 다음과 같은 캐시 관련 dependency가 pom.xml에 만들어지게 된다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

위의 디펜던시는 기본적인 cache 기능을 사용할 수 있게 도와주는데, 별도의 선언이 없다면 로컬 메모리에 저장되는 ConcurrentMapCacheManager가 자동으로 빈에 등록되어 사용된다.

 

물론 이는 별도의 모듈을 선언하여 사용할 수 있다.

대표적인 예시로는 자주 사용되는 ehcache가 있다.

 

ehcache 사용을 원한다면 다음의 디펜던시를 추가하고 xml과 Configuration 클래스 설정 후

spring.cache.jcache.config=classpath:{filename}.xml 로 설정해주면 된다.

<dependency>
   <groupId>net.sf.ehcache</groupId>
   <artifactId>ehcache</artifactId>
   <version>2.10.6</version>
</dependency>

 

 

 

그 후에는 application.properties에 기본적인 설정들을 추가한다.

server.address=0.0.0.0

spring.jpa.show-sql=true
spring.jpa.generate-ddl=false

spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://${linuxServer}:3306/test
spring.datasource.username=root
spring.datasource.password=0000

 

 

 

기본 캐시 사용방법

 

가장 먼저 메인 클래스에 @EnableCaching 어노테이션을 추가한다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CachetestApplication {

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

}

 

그리고 domain과 repository 정도를 설정해준다.

package com.example.demo.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="test_table")
public class TestDomain {
	@Id
	@Column(name="id")
	private int id;
	@Column(name="col1")
	private String col1;
	@Column(name="col2")
	private String col2;
	@Column(name="col3")
	private String col3;
	@Column(name="col4")
	private String col4;
	@Column(name="col5")
	private String col5;
	@Column(name="col6")
	private String col6;
	@Column(name="col7")
	private String col7;
	@Column(name="col8")
	private String col8;
	@Column(name="col9")
	private String col9;
	@Column(name="col10")
	private String col10;
}
package com.example.demo.repository;

import java.util.List;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.domain.TestDomain;

@Repository
public interface TestRepository extends CrudRepository<TestDomain, Integer> {
	// iterable > list 변환을 위해 명시
	public List<TestDomain> findAll();
}

 

다음으로 사용자와 주고받을 컨트롤러를 작성한다.

추후 작성할 CacheService를 불러와, 데이터 전체를 SELECT 한 결과를 받을 것이고, 이에 대한 갯수 정도만 반환한다.

*시간 비교를 위해 조금이라도 더 시간을 끌어보고자 Repository의 count() 메소드를 사용하지 않았다.

*그런데 캐시는 결국 둘다 1ms로 큰 의미는 없는 행동이었다.

package com.example.demo.controller;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.service.CacheService;

@RestController
public class CacheController {
	@Autowired CacheService cacheService;
	Logger log = LoggerFactory.getLogger(CacheController.class);
	
	@GetMapping("/cache/read")
	public long getCacheCount() {

		long now = new Date().getTime(); //millisecond 저장
		long count = cacheService.getAllData().size();
		long after = new Date().getTime();
		
		
		log.info("CacheController.getCacheCount() Method done in : {} millisec", after - now);
		
		return count;
	}
}

 

이제 서비스를 작성한다.

 

가장 먼저 실행할 어노테이션은 @Cacheable이다.

@Cacheable은 파라미터로 value을 받을 수 있는데, 만약 value에 해당하는 값이 캐시에 존재한다면 해당 값을 반환하고, 그렇지 않다면 함수 내용을 수행하여 값을 반환하게 된다.

 

코드는 아래와 같다.

package com.example.demo.service;

import java.util.Date;
import java.util.List;

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

import com.example.demo.domain.TestDomain;
import com.example.demo.repository.TestRepository;

import org.slf4j.Logger;

@Service
public class CacheService {
	Logger log = LoggerFactory.getLogger(CacheService.class);
	
	@Autowired TestRepository repo;
	
	@Cacheable(value = "count")
	public List<TestDomain> getAllData() {
		log.info("CacheService.getCount() INIT");
		return repo.findAll();
	}
}

추가로 키도 설정이 가능하다.

ex) @Cacheable(value = "도메인명", key="#파라미터명")

 

 

이제 해당 URL로 접속을 수행한다면 아래와 같은 로그를 확인할 수 있다.

 

CacheService 클래스의 getCount() 함수가 실행되었고, 실행 시간은 244밀리초가 소요되었다.

그렇다면 한 번 더 해당 서비스를 접속하면 어떨까?

이미 Cache에 데이터가 저장된 상태이기 때문에 함수는 실행되지 않았고, 실행 시간 또한 고작 1 밀리초도 소요되지 않았다.

 

하지만 이 상태에서 DB에 데이터를 추가로 삽입하면 어떻게 될까?

INSERT INTO test_table VALUES (10001, 10001, 10001, 10001, 10001, 10001, 10001, 10001, 10001, 10001, 10001);

위의 SQL을 실행한 후 다시 한 번 위의 URL을 호출해보았다. 하지만 결과는 이전과 같은 10,000 이었다.

 

이유라면 이미 데이터를 불러와서 캐시에 저장한 상태이고, 갱신 작업은 없었기 때문이다.

 

그래서 캐시를 주기적으로 갱신하기 위한 작업이 필요한데, 이 때 사용되는 것이 바로 @CachePut 어노테이션이다.

@CachePut은 캐시가 존재하면 실행하지 않는 @Cacheable과 달리 반드시 내부 로직을 실행하며, 캐시 내용을 수정하는 작업을 담당한다.

 

@CachePut은 다음과 같이 사용할 수 있다. 또한 @Cacheable과 마찬가지로 key도 설정할 수 있다.

@CachePut(value = "count")
public List<TestDomain> getAllDataPut() {
	log.info("CacheService.getCountPut() INIT");
	return repo.findAll();
}

 

이제 해당 함수를 호출하는 컨트롤러를 만들어 실행시켜보면, return 값이 10,001로 갱신되었고, 앞서 만든 컨트롤러에 접속해도 마찬가지로 갱신된 10,001이 정상 출력됨을 확인할 수 있다.

 

 

다음은 @CacheEvict인데, 이 함수는 캐시의 삭제를 담당한다.

아래와 같이 만들어 한 번 수행해보자.

@CacheEvict(value = "count")
public void getAllDataEvi() {
	log.info("CacheService.getAllDataEvi() INIT");
}

위의 함수를 실행시킨 뒤 다시 @Cacheable 함수를 실행시켜보면 캐시가 삭제되어 다시 한 번 DB에서 데이터들을 불러오는 것을 확인할 수 있다.

 

 

 

 

 

+) 갑자기 생긴 궁금증

어차피 프로그램이 종료되면 삭제되는 데이터이니, 변수로 만들면 속도 차이가 어떨까?

 

궁금증 해결을 위하여 생성 후 모든 데이터를 불러와 변수로 저장하는 새로운 서비스를 만들어주었다.

package com.example.demo.service;

import java.util.List;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.domain.TestDomain;
import com.example.demo.repository.TestRepository;

@Service
public class LocalService {
	Logger log = LoggerFactory.getLogger(LocalService.class);

	@Autowired TestRepository repo;
	
	List<TestDomain> testDomainList;
	
	@PostConstruct
	public void setTestDomainList() {
		this.testDomainList = repo.findAll();
	}
	
	public List<TestDomain> getTestDomainList() {
		log.info("LocalService.getTestDomainList() INIT");
		return testDomainList;
	}
	public List<TestDomain> updateTestDomainList() {
		log.info("LocalService.updateTestDomainList() INIT");
		this.testDomainList = repo.findAll();
		return testDomainList;
	}
	

}

 

결과는 캐시를 사용할 때와 다르지 않았다.

*데이터가 적어서 그런가 싶어 1,000,000 개까지 데이터를 늘려보아도 결과는 다르지 않았다.

 

 

+) 주의점 : 동일 클래스 내에서 Cacheable 함수 호출 시 해당 함수는 항상 수행된다.

https://ifuwanna.tistory.com/202

728x90
반응형

'실습 > 리눅스 서버 + 스프링 부트' 카테고리의 다른 글

Redis In Java_자료구조  (0) 2022.03.20
Spring Cloud Config  (0) 2022.03.13
httpd (아파치 웹 서버)  (0) 2022.03.06
Filter, Interceptor, AOP  (0) 2022.02.01
ntp를 이용한 타임서버 구축  (0) 2022.01.22