[SpringBoot] Local-Memory 캐시를 사용해보자
@Service
public class PathService {
private static final Logger logger = LoggerFactory.getLogger(PathService.class);
private final LineService lineService;
private final StationService stationService;
private final PathFinder pathFinder;
...
@Transactional(readOnly = true)
public PathResponse findPath(Long departure, Long arrival, LoginMember loginMember) {
try {
logger.info("캐시 되에에엠~");
List<Line> lines = lineService.findLines();
Station departureStation = stationService.findStationById(departure);
Station arrivalStation = stationService.findStationById(arrival);
SubwayPath subwayPath = pathFinder.findPath(lines, departureStation, arrivalStation);
return PathResponseAssembler.assemble(subwayPath, loginMember.getAge());
} catch (Exception e) {
throw new InvalidPathException(e.getMessage());
}
}
}
최단 경로를 찾는 알고리즘을 사용해, 역과 역사이의 최단 거리와 요금을 계산하는 로직이에요.
매번 모든 데이터 DB를 통해 불러와요. → 사용자가 많을 시 DB connection의 수가 많아져 성능의 저하가 일어날 거에요.
이때 메모리에 데이터를 저장하여 메모리에서 가져다 쓰게 하면 DB 커넥션이 줄어 성능상으로 더 효율이 좋을 거에요.
위의 로직에서 수정, 삭제의 경우는 조회보다 그 빈도수가 많지 않으리라 생각해요. (수정, 삭제가 빈번한 곳에서는 캐싱이 비효율적일 수 있음)
이를 위해 캐시를 사용해요.
스프링에서 제공하는 기본 Cache
spring 3.1버전부터 Spring Application에 캐시를 쉽게 추가할 수 있도록 기능을 제공해요.
유사 트랜잭션을 지원하고, 사용하고 있는 코드(메소드)에 영향을 최소화하면서 일관된 방법으로 캐시를 사용 할 수 있게 되었어요.
Spring에서 캐시 추상화는 메소드를 통해 기능을 지원하는데, 메소드가 실행되는 시점에 파라미터에 대한 캐시 존재 여부를 판단하여 없으면 캐시를 등록하게 되고, 캐시가 있으면 메소드를 실행시키지 않고 캐시 데이터를 Return 해주게 돼요.
Spring 캐시 추상화를 지원하기 때문에 개발자는 별도의 캐시 로직을 작성하지 않아도 돼요. 하지만 캐시를 저장하는 저장소는 직접 설정을 해줘야 해요. Spring에서는 CacheManager
라는 Interface를 제공하여 캐시를 구현하도록 하고 있어요.
별다른 의존성을 추가하지 않을 시, Local-Memory에 저장이 가능한 ConcurrentMap
기반인 ConcurrentMapCacheManager
가 Bean으로 자동 등록돼요(org.springframework:spring-context에서 지원).
이 단계에서는 별도의 저장소(EhCache, Redis)를 사용하지 않을 거라, CacheManager를 직접 구성할 수 있는 spring-boot-starter-cache
는 추가하지 않아요.
서론이 길었네요. 일단 사용해봐요.
캐시 설정 등록
@SpringBootApplication
@EnableCaching
public class SubwayApplication {
public static void main(String[] args) {
SpringApplication.run(SubwayApplication.class, args);
}
}
@EnalbeCaching
을 등록해요. @Configuration
에서 등록해줘도 무방해요.
캐시 저장
@Service
public class PathService {
private static final Logger logger = LoggerFactory.getLogger(PathService.class);
private final LineService lineService;
private final StationService stationService;
private final PathFinder pathFinder;
...
@Transactional(readOnly = true)
@Cacheable(value = "cache::shortestPath", key = "#departure.toString() + '::' + #arrival.toString() +'::' + #loginMember.age")
public PathResponse findPath(Long departure, Long arrival, LoginMember loginMember) {
try {
logger.info("캐시 되에에엠~");
List<Line> lines = lineService.findLines();
Station departureStation = stationService.findStationById(departure);
Station arrivalStation = stationService.findStationById(arrival);
SubwayPath subwayPath = pathFinder.findPath(lines, departureStation, arrivalStation);
return PathResponseAssembler.assemble(subwayPath, loginMember.getAge());
} catch (Exception e) {
throw new InvalidPathException(e.getMessage());
}
}
}
@Cacheable
을 통해 캐시할 메서드를 지정해요.
처음보는 것들이 많네요. 각각에 대해 알아보죠.
@Cacheable
캐싱하려는 메서드를 지정하기 위해 사용해요.
value, cacheNames: 캐시이름
key: 같은 캐시명을 사용 할 때, 구분되는 구분 값 (KeyGenerator와 함께 쓸 수 없음). 별도 지정이 없을 시 파라미터로 key를 지정.
keyGenerator: 특정 로직에 의해 cache key를 만들고자 하는 경우 사용. 4.0이후 버전 부터 SimpleKeyGenerator를 사용. Custom Key Generator를 사용하고 싶으면, KeyGenerator 인터페이스를 별도로 구현
cacheManager: 사용할 CacheManager를 지정 (EHCacheManager, RedisCacheManager등)
cacheResolver: Cache 키에 대한 결과값을 돌려주는 Resolver (Interceptor역할). CacheResolver를 구현하여 Custom하게 처리 할 수도 있음
condition: SpEL 표현식을 통해 특정 조건에 부합하는 경우에만 캐시 사용. and, or 표현식등을 통해 복수 조건 사용가능. 연산 조건이 true인 경우에만 캐싱
unless: 캐싱이 이루어지지 않는 조건을 설정. 연산 조건이 true 이면 경우에는 캐싱되지 않음. ex) id가 null아 아닌 경우에만 캐싱 (unless = "#id == null")
sync: 캐시 구현체가 Thread safe 하지 않는 경우, 자체적으로 캐시에 동기화를 거는 속성. default는 false
위에서는 캐시이름을 cache::shortestPath
로, key값을, #departure.toString() + '::' + #arrival.toString() +'::' + #loginMember.age
로 뒀어요. (LoginMember는 age 필드를 가지고 있어요)
키를 따로 설정하지 않으면 전체 파마리터가 키가 돼요.
특정 파라미터만 적용하고 싶다면 "#departur.toString()"처럼 지정할 수 있고, 여러개의 키를 사용하고 싶다면 + 를 통해 추가할 수 있어요,
저는 로직에 필요한 정보들을 key로 뒀어요.
캐시 업데이트
캐시를 하는 것 까진 좋았는데, 데이터가 업데이트 될 때(노선이 추가되서 최단경로가 바뀜) 이전 캐시값이 남아있으면 안되겠죠? 이때 사용하는 것이 @CacheEvict
이에요.
@Repository
@CacheConfig(cacheNames = {"cache::shortestPath"})
public class LineDao {
private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert insertAction;
...
@CacheEvict(allEntries = true)
public Line insert(Line line) {
Map<String, Object> params = new HashMap<>();
params.put("id", line.getId());
params.put("name", line.getName());
params.put("color", line.getColor());
params.put("extra_fare", line.getExtraFare());
Long lineId = insertAction.executeAndReturnKey(params).longValue();
return new Line(lineId, line.getName(), line.getColor(), line.getExtraFare());
}
@CacheEvict(allEntries = true)
public void update(Line newLine) {
String sql = "update LINE set name = ?, color = ? where id = ?";
jdbcTemplate.update(sql, newLine.getName(), newLine.getColor(), newLine.getId());
}
...
}
라인이 새로 생성될 때, 업데이트 될때 @CacheEvict을 통해 저장된 캐시를 지울 수있어요.
@CacheEvict
메서드 실행시 설정값에 따른 캐시를 삭제해요.
value, cacheNames: 캐시이름
key: 같은 캐시명을 사용 할 때, 구분되는 구분 값 (KeyGenerator와 함께 쓸 수 없음). 별도 지정이 없을 시 파라미터로 key를 지정.
keyGenerator: 특정 로직에 의해 cache key를 만들고자 하는 경우 사용. 4.0이후 버전 부터 SimpleKeyGenerator를 사용. Custom Key Generator를 사용하고 싶으면, KeyGenerator 인터페이스를 별도로 구현
cacheManager: 사용할 CacheManager를 지정 (EHCacheManager, RedisCacheManager등)
cacheResolver: Cache 키에 대한 결과값을 돌려주는 Resolver (Interceptor역할). CacheResolver를 구현하여 Custom하게 처리 할 수도 있음
condition: SpEL 표현식을 통해 특정 조건에 부합하는 경우에만 캐시 사용. and, or 표현식등을 통해 복수 조건 사용가능. 연산 조건이 true인 경우에만 캐싱
allEntries
: Cache Key에 대한 전체 데이터 삭제 여부. default는 false
beforeInvocation
: true면 메서드 실행 이전에 캐시 삭제, false면 메서드 실행 이후 삭제, default는 false
저는 라인이 업데이트 될때, @CacheConfig()
에서 cacheNames = {"cache::shortestPath"}
으로 지정된 모든 캐시를 제거했어요.
@CacheConfig
클래스 단위로 캐시설정을 동일하게 하는데 사용해요.(클래스 전역 설정)
이 설정은 CacheManager가 여러개인 경우에만 사용해요. 즉 라인 조회 클래스에서는 Redis기반 캐시를 사용하고, 역 조회 클래스에서는 EHCache 기반 캐시를 사용할 때 각 클래스 별로 CacheManager를 지정이 가능해요
cacheNames: 캐시이름
keyGenerator: 특정 로직에 의해 cache key를 만들고자 하는 경우 사용. 4.0이후 버전 부터 SimpleKeyGenerator를 사용. Custom Key Generator를 사용하고 싶으면, KeyGenerator 인터페이스를 별도로 구현
cacheManager: 사용할 CacheManager를 지정 (EHCacheManager, RedisCacheManager등)
cacheResolver: Cache 키에 대한 결과값을 돌려주는 Resolver (Interceptor역할). CacheResolver를 구현하여 Custom하게 처리 할 수도 있음
이외의 애노테이션
@CachePut
메서드 실행에 영향을 주지 않고 캐시를 갱신해야 하는 경우 사용해요.
보통은 @Cacheable
과 @CachePut
Annotation을 같이 사용하지 않아요. (둘은 다른 동작을 하기 때문에, 실행순서에 따라 다른 결과가 나올 수 있음)
@CachePut
Annotation은 캐시 생성용으로만 사용해요.
@CachePut(value="addresses")
@Caching
@CacheEvict
이나 @CachePut
을 여러개 지정해야 하는 경우에 사용해요.
@CacheEvict
, @CachePut
, @Cacheable
는 함께쓰지 않아요.
주로 여러가지의 key에 대한 캐시를 중첩적으로 삭제해야 할 때 사용해요.
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key = "#p0") })
사용시 주의점
Spring @Cacheable은
내부적으로 Spring AOP를 이용하기 때문에 @Async
, @Transactional
등과 마찬가지로 아래와 같은 제약사항을 가져요.
pulbic method에만 사용가능
같은 객체내의 method끼리 호출시에는 @Cacheable이 설정되어있어도 캐싱되지 않음
Bean에서만 사용가능 (이말은 Bean이 의존하고있는 객체에서는 동작하지 않는다는 것)
구현 클래스에서만 사용하길 권장 (인터페이스에(혹은 인터페이스 메서드) @Cache* 어노테이션을 붙일 수 있지만, 인터페이스에 기반을 둔 프락시를 사용할 때만 원하는 대로 동작하기 때문) → 이거때문에 많이 삽질했어요. ㅠㅠ
Refer
https://stackoverflow.com/questions/36977643/spring-cache-not-working-for-abstract-classes
https://jeong-pro.tistory.com/170
https://www.baeldung.com/spring-cache-tutorial
https://spring.io/guides/gs/caching/
https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache
https://coding-start.tistory.com/324
https://blog.outsider.ne.kr/1094
https://medium.com/finda-tech/spring-로컬-캐시-라이브러리-ehcache-4b5cba8697e0
https://jaehun2841.github.io/2018/11/07/2018-10-03-spring-ehcache/#들어가며
https://www.baeldung.com/spring-cache-tutorial
http://dveamer.github.io/backend/SpringCacheable.html