Spring 환경에서 캐싱 기능을 구현하기 위해 Redis + Jedis 또는 Redis + Lettuce를 통한 구현을 권장하고 있습니다. 둘 중 어느 것을 사용해야 할까요? redis 공식 홈페이지의 글을 살펴보면 "본인이 선택하기 나름"이라고 합니다. Jedis를 사용하면 구현이 간단하지만 기능적으로 확장성(Scalability)이 떨어집니다. Lettuce를 사용하면 Jedis와 비교해서 구현이 복잡하지만 기능적으로 확장성이 뛰어납니다. 여기서 말하는 '확장성'은 동시다발적인 요청을 얼마나 유연하게 처리할 수 있느냐를 말하는 것 같습니다.
저의 경우 확장성 보다는 캐싱 기능 구현을 통한 학습에 중점을 두고 있기 때문에 Jedis로 구현했습니다. 일단 캐싱 기능이 어떻게 작동되는지 빠르게 확인하고 싶었기 때문입니다.
Jedis 사용에 대한 정보는 스프링 공식 문서 혹은 Jedis 깃 리포지토리에서 확인 가능합니다.
개발환경
- Spring Legacy Project(MVC)
- Spring 5.0.7
- Maven
- Java 1.8
- Oracle 11g
- Redis-x64-3.2.100
먼저 redis 서버를 설치해야 합니다. 아래 링크에서 .msi 파일을 다운로드한 후 설치를 진행하면 됩니다.
설치 후 커맨드 창을 열어서 redis-cli 명령어를 실행하면 redis 서버로 접속할 수 있습니다.
https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100
의존성 추가를 해줍니다. 저는 Maven 프로젝트이기 때문에 pom.xml에 필요한 의존성을 추가했습니다. Spring 5.0.7, Java 1.8 환경에서 아래의 설정이 정상 작동하는 것을 확인했습니다.
pom.xml
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.23.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.10.2</version>
</dependency>
XML 설정을 통해 빈을 관리하고 있기 때문에 Redis 설정도 XML 파일 형태로 작성했습니다. 작성하면서 느꼈던 점은, Java 클래스를 통한 설정이 훨씬 직관적이고 관리하기 쉽겠다는 것입니다. XML 파일도 계속 보다 보면 익숙해지지만, Java 클래스 설정과 비교하면 차이가 큰 것 같습니다.
redis 관련 설정은 redis-context.xml 파일을 새로 생성해서 관리하도록 했습니다.
redis-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--redis 설정 정보 파일 불러오기 -->
<context:property-placeholder location="/WEB-INF/redis/redis.properties" ignore-unresolvable="true" />
<!-- JedisPoolConfig 설정 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxTotal" value="${redis.maxActive}" />
<property name="maxWaitMillis" value="${redis.maxWait}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<!-- JedisConnectionFactory 설정 -->
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.host}" />
<property name="port" value="${redis.port}" />
<property name="database" value="${redis.dbIndex}" />
<property name="poolConfig" ref="poolConfig" />
</bean>
<!-- RedisTemplate 설정 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
<!-- RedisCacheManager 설정 -->
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate" />
<property name="defaultExpiration" value="${redis.expiration}" />
</bean>
<!-- RedisCacheConfig 설정 -->
<bean id="redisCacheConfig" class="com.board.util.RedisCacheConfig">
<constructor-arg ref="jedisConnectionFactory" />
<constructor-arg ref="redisTemplate" />
<constructor-arg ref="redisCacheManager" />
</bean>
</beans>
redis-context.xml 설정에 대한 세부 내용입니다.
JedisPoolConfig
Jedis 인스턴스는 싱글 스레드입니다. 멀티 스레드 환경에서 단일 Jedis 인스턴스를 공유하려고 하면 thread-safety 관련 문제가 발생합니다.(스레드가 안전하지 않음 = 의도하지 않은 결과 발생) 따라서 멀티 스레드 환경에서의 Jedis 사용 방법을 고민해야 합니다.
JedisPool은 멀티 스레드 환경에서 Jedis 커넥션을 재활용 할 수 있게 해 줍니다. pool 객체의 getResource() 함수로 단일 Jedis 인스턴스를 가져와서 이용하는 방식으로 사용합니다.
JedisPoolConfig에 대한 세부 설정입니다.
- maxTotal : pool에서 할당 가능한 최대 연결 수.
- maxIdle : 추가 커넥션을 해제하지 않고 pool에서 유휴 상태로 유지할 수 있는 최대 연결 수.
- maxWaitMilllis : 사용 가능한 커넥션이 없을 때 호출자가 대기해야 하는 최대 시간.
- testOnBorrow : pool에서 빌리기 전에 ping 명령을 사용해서 연결의 유효성을 검사할지의 여부.
기타 설정 내용은 관련 문서를 참고하시면 됩니다.
JedisConnectionFactory
Redis 연결 정보를 설정합니다. 여기서 설정한 정보는 RedisTemplate에 적용되어 함께 작동합니다.
위 xml 파일 설정에서는 Redis 연결 비밀번호가 설정되지 않았습니다. 만약 설정하고 싶다면 비밀번호 설정 관련 property를 추가하면 됩니다.
RedisTemplate
RedisTemplate은 Redis 사용에 중요한 역할을 합니다. 전달 받은 자바 객체를 serialization/deserialization 기능을 통해 key,value 형태로 Redis 메모리에 저장하거나 가져옵니다. RedisTemplate에서 자바 객체 serialization 작업을 할 때 기본적으로 JdkSerializationRedisSerializer 클래스를 사용합니다. RedisTemplate에 대한 보다 자세한 설명은 공식 문서에서 확인할 수 있습니다.
RedisCacheManager
RedisCacheManager를 빈으로 등록하면 Spring에서 캐싱을 할 때 로컬 캐시에 저장하지 않고 Redis에 저장합니다.
RedisCacheConfig
캐시 설정을 담당하는 RedisCacheConfig 클래스에 JedisConnectionFactory, RedisTemplate, RedisCacheManager 생성자를 이용한 객체 주입을 합니다.
<context:property-placeholder location="/WEB-INF/redis/redis.properties" ignore-unresolvable="true" />
위 xml 파일에서 property 설정 시 value 설정을 ${redis.maxIdle} 이렇게 했는데, 이는 redis.properties 파일에서 값을 가져오도록 설정한 것입니다. <context:property-placeholder> 태그를 사용하면 외부 설정 프로퍼티 파일을 불러올 수 있습니다. 그리고 태그에서 ignore-unresolvable="true"로 설정했는데, 이 설정은 프로퍼티 파일이 독립적으로 인식될 수 있게 만드는 것 같습니다. 작업을 하다보면 프로퍼티 파일을 여러 개 불러오는 경우가 있는데, 이 설정을 하지 않으면 에러가 발생한다고 합니다. 참고로 아무 설정을 하지 않으면 ignore-unresolvable="false"로 적용됩니다.
redis.properties
redis.host=사용IP
redis.port=6379
redis.pass=비밀번호
redis.dbIndex=0
redis.expiration=3000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
RedisCacheConfig.java
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
protected final static Logger log = LoggerFactory.getLogger(RedisCacheConfig.class);
private volatile JedisConnectionFactory mJedisConnectionFactory;
private volatile RedisTemplate<String, String> mRedisTemplate;
private volatile RedisCacheManager mRedisCacheManager;
public RedisCacheConfig() {
super();
}
public RedisCacheConfig(JedisConnectionFactory mJedisConnectionFactory,
RedisTemplate<String, String> mRedisTemplate, RedisCacheManager mRedisCacheManager) {
super();
this.mJedisConnectionFactory = mJedisConnectionFactory;
this.mRedisTemplate = mRedisTemplate;
this.mRedisCacheManager = mRedisCacheManager;
}
public JedisConnectionFactory redisConnectionFactory() {
return mJedisConnectionFactory;
}
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory cf) {
return mRedisTemplate;
}
public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
return mRedisCacheManager;
}
// 캐시의 key를 생성(중복X)
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName());
sb.append(method.getName());
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}
기본 설정은 모두 끝났습니다. 이제 캐시 기능을 사용할 메소드에 어노테이션을 적용합니다. 어노테이션을 적용하면 한 번 이상 호출된 메소드는 캐싱 서버에 저장되어 DB가 아닌 캐싱 서버에서 결과를 호출합니다. 어노테이션 적용은 일반적으로 Service 계층의 메소드에 적용됩니다.
- @Cacheable : 캐시 서버에 데이터가 없는 경우 원래대로 DB에서 데이터를 조회해서 결과를 출력하고, 캐시 서버에 데이터가 있으면 서버에 저장된 데이터를 반환합니다. @Cacheable("캐시 이름") 형식으로 캐시 이름을 지정할 수 있습니다. 캐시 이름 하위에 key-value 형태로 데이터를 저장합니다.
@Cacheable("productList") // 캐시 이름은 productList
public Product getProductList(String category) { ... }
// key-value에서 key는 메소드 파라미터인 category, 메소드의 결과는 value.
예를 들어 getProductList("신발") 형태로 메소드가 호출되었다고 가정해보자.
step 1) 캐시 서버에 getProductList("신발")에 해당하는 값이 있는 확인한다.
step 2) 해당 메소드를 처음 호출하기 때문에 값이 없다. 따라서 DB를 통해 값을 반환한다.
step 3) 반환된 값을 캐시 서버에 getProductList("신발")의 value로 저장한다.
step 4) 메소드가 getProductList("신발") 형태로 또 다시 호출되었다.
이제 캐시 서버에 해당하는 값이 있으므로 로직을 실행하는 대신 캐시 서버에서 조회한 값을 반환한다.
- @CachePut : 캐시 서버에 값을 저장하는 용도로만 사용합니다. 실행 결과를 캐시 서버에 저장하지만, 조회할 때 저장된 캐시의 내용을 사용하지 않습니다. 따라서 호출된 메소드의 로직은 원래대로 실행됩니다.
- @CacheEvict : 캐시를 제거하기 위해 사용합니다. 일반적으로 캐시 생명 주기를 설정해서 관리하지만, 값이 자주 바뀌는 경우라면 이 어노테이션을 통해 캐시를 제거할 수 있습니다. 참고로 캐시 제거는 메소드의 key에 해당하는 캐시를 제거합니다.
어노테이션에 사용되는 옵션이 많습니다. 관련 내용은 공식 문서에서 확인할 수 있습니다.
저는 서비스 메인 페이지에 등록된 모든 제품이 출력되도록 구현했습니다. 캐싱 기능이 없다면 페이지를 로드 할 때마다 DB에서 가져와야 합니다. 등록 상품이 많아질수록 페이지 출력 속도 및 DB 부하가 증가할 것입니다. 그래서 메인 페이지 출력을 담당하는 메소드에 @Cacheable 어노테이션을 적용하고 캐시 서버에서 결과를 쉽게 확인하기 위해 캐시 이름을 설정했습니다.
@Override
@Cacheable("productListCache")
public List<ProductVO> getProductList(Criteria cri) {
// 캐싱 기능이 제대로 동작한다면 최초 한 번만 메시지가 출력된다.
System.err.println("productListCache");
}
실행 결과 메소드 내부에 설정된 메시지가 최초 한 번만 호출됩니다. 즉 로직을 실행하지 않고 캐시 서버를 통해 값을 반환하고 있다는 것을 의미합니다. 좀 더 정확히 확인하기 위해 커맨드 창을 열어서 서버에 캐시가 저장되어 있는지 확인해봅니다. 페이징 기능이 적용되어 있기 때문에 이동한 페이지마다 캐싱 데이터가 저장되었습니다.
후기
이번 프로젝트의 목표는 '최대한 실제로 서비스 가능한 서비스를 만들기' 입니다. 그렇게 하려면 안전하고 탄탄한 서비스를 만들어야 합니다. 캐싱 기능 적용은 탄탄함을 더하기 위한 작업이었습니다. 캐싱 기능을 적용하면 좀 더 효율적인 서비스가 될 수 있다고 생각했습니다. 아직 저장된 데이터의 숫자가 적어서 눈에 띄는 속도의 변화는 없습니다. 데이터를 대량으로 넣어서 메소드 실행 속도를 측정해야겠습니다.
참고
https://redis.com/blog/jedis-vs-lettuce-an-exploration/
https://github.com/redis/jedis/wiki/Getting-started
https://gist.github.com/JonCole/925630df72be1351b21440625ff2671f
https://programmer.group/spring-extreme-speed-integrated-annotation-redis-practice.html
'Programming > 개인 프로젝트' 카테고리의 다른 글
[중고거래장터 - Study&Refactoring] 캐싱 기능 적용(Spring MVC + Redis + Jedis) Java 클래스 방식 (0) | 2022.03.31 |
---|---|
[중고거래장터 - Study&Refactoring] Oracle ~> MySQL로 이동 (0) | 2022.03.22 |
[중고거래장터 -5] URL 디자인 그리고 Spring Security (0) | 2022.02.16 |
[중고거래사이트 - 4] 제대로 만든다는 것은 무엇일까 (0) | 2022.02.12 |
[중고거래사이트 - 3] MockMVC를 통한 테스트 (0) | 2022.02.10 |