Dot Programming/Spring

[Spring] Spring에서 Local-Memory로 간단하게 Cache 사용하기

루지 2022. 4. 17. 00:44

    Local-Memory로 간단하게 Cache 사용하기

    Spring 3.1버전부터 캐시를 쉽게 추가할 수 있도록 기능을 제공하고 있다. 트랜잭션(@Transactional) 기능과 유사하게 캐싱 추상화 기능을 통해 코드에 미치는 영향을 최소화하면서 다양한 캐싱 방법을 사용할 수 있다.

     

    spring-context에서 기본으로 제공하므로 Redis나 다른 캐시 저장소를 사용할 것이 아니라면 특정한 설정은 필요없다. Spring 앱 위에 @EnableCaching을 명시해주면 캐싱 기능 사용이 가능하다. 

    @SpringBootApplication
    @EnableCaching
    public class SpringCacheApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(SpringCacheApplication.class, args);
    	}
    }

     

    Cache Storage

    CacheManager로 사용할 Cache Storage를 선택할 수 있다. 아무런 설정도 하지 않을 경우 스프링은 디폴트로 ConcurrentMapCache를 사용한다.

    • ConcurrentMapCacheManager: JDK의 ConcurrentHashMap을 사용해 구현한 캐시
    • SimpleCacheManager: 기본적으로 제공하는 캐시가 없어 사용할 캐시를 직접 등록하여 사용하기 위한 캐시
    • EhCacheCacheManager: 자바에서 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시
    • CompositeCacheManager: 1개 이상의 캐시 매니저를 사용하도록 지원해주는 Mixed 캐시
    • CaffeineCacheManager: Java 8로 Guava 캐시를 재작성한 Caffeine 캐시를 사용하는 캐시 
    • JCacheCacheManager: JSR-107 기반의 캐시를 사용하는 캐시 
    @Configuration
    public class AppConfig {
        public CacheManager cacheManager() {
             // configure and return an implementation of Spring's CacheManager SPI
             SimpleCacheManager cacheManager = new SimpleCacheManager();
             cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
             return cacheManager;
        }
    }

     

    Look-Aside/ Write-Around 캐싱 전략

    Spring Local-Memory로 다뤄 볼 캐싱 전략은 다음 그림과 같다. Look-Aside/ Write-Around 전략은 범용적으로 가장 많이 쓰인다.

    • Look-Aside 읽기 전략은 데이터를 찾을 때 캐시를 먼저 확인하고 캐시가 존재하면 바로 반환한다. 만약 캐시에 존재하지 않으면 DB에 조회해서 반환한다.
    • Write-Around 쓰기 전략은 데이터를 저장할 때 캐시를 거치지 않고 바로 DB 디스크 쓰기 작업만 이루어지도록 한다.

    Look-Aside 읽기 전략
    Write-Around 쓰기 전략

     

    멤버 저장하기

    먼저 Write-Around 전략으로 멤버를 생성할 때는 DB에만 저장되도록 작성한다.

    • 참고로 spring-data-jpa, mysql-connector 의존성을 추가하여 Mysql DB와 연동한 상태이다.
    @PostMapping
    public Member save(@RequestBody Member member) {
        log.info("save to DB :" + member);
        return memberRepository.save(member);
    }

     

    @Cacheable

    캐시할 메서드 위에 선언한다. key는 SpEL을 사용해서 설정하고 싶은 속성만 선택할 수 있다. @Cacheable을 Get 메소드에 명시하면 자동으로 Look-Aside 읽기 전략으로 동작한다.

    • (Cache Hit) 만약 캐시에 해당 정보가 있으면 캐시에서 해당 정보를 꺼내서 반환한다.
    • (Cache Miss) 없으면 DB에서 읽어온 데이터를 캐시에 저장한다.
    @GetMapping("/{id}")
    @Cacheable(key = "#id", cacheNames = "Member")
    public Member findOne(@PathVariable Long id) {
        Member member = memberRepository.findById(id).orElseThrow(() -> new RuntimeException("해당 멤버는 존재하지 않습니다."));
        log.info("Member fetching from DB :" + id);
        return member;
    }

     

    DB에 id:1인 member가 저장되어 있는 상태이다. 위의 API를 이용해 첫 조회를 한 경우에만 DB에서 조회하고 이후 조회부터는 캐싱에서 가져오기 때문에 해당 로그가 뜨지 않는다. 

    • @Cacheable은 Cache Hit되면 메서드를 실행하지 않고 캐싱된 데이터를 바로 반환한다.

    첫 Member:1 조회 (Cache miss)

     

    @CacheEvict

    캐시를 삭제할 메서드 위에 선언한다. DB와 Cache에 저장된 데이터를 모두 삭제한다. 

    @DeleteMapping("/{id}")
    @CacheEvict(key = "#id", cacheNames = "Member")
    public String deleteOne(@PathVariable Long id) {
        memberRepository.deleteById(id);
        log.info("Member delete from Cache :" + id);
        return "delete";
    }

     

     

    @CachePut

    메서드 실행을 방해하지 않고 캐시 내용을 업데이트 할 수 있다. 즉, 메서드가 실행되고 결과가 캐싱된다.

    • @Cacheable과의 차이점은 @Cacheable은 Cache Hit인 경우 메서드 실행을 건너뛰지만 @CachPut은 실제로 메서드를 실행한 다음 그 결과를 캐싱한다.
    @PutMapping("/{id}")
    @CachePut(key = "#id", cacheNames = "Member")
    public Member update(@PathVariable Long id, @RequestBody Member member) {
        Member findMember = memberRepository.findById(id).orElseThrow(() -> new RuntimeException("해당 멤버는 존재하지 않습니다."));
    
        findMember.update(member);
        memberRepository.save(findMember);
    
        log.info("Member update  :" + id);
        return findMember;
    }

     

    업데이트하면 DB와 Cache에 동시에 적용된다. 즉, 메서드가 실행된 후 반환되는 값을 캐시에도 적용해준다.

     

    Member:1 업데이트

     

    캐시에 조회해보면 변경된 값을 확인할 수 있다.

     

    Member:1 조회

    @Caching

    이 애노테이션은 동일한 캐싱 애노테이션을 여러 개 그룹화하여 사용할 수 있다. @Cacheable, @CacheEvict, @CachePut 을 함께 사용할 수는 없다.

    @DeleteMapping("/{id}")
    @Caching(evict = {
        @CacheEvict(key = "#id", cacheNames = "Member"),
        @CacheEvict(key = "#id", cacheNames = "Product")
    })
    public String deleteMebmerAndProduct(@PathVariable Long id) {...}

     

    Spring Cache 키 생성시 유의사항

    int, long 과 같은 primitive type의 파라미터만 사용하는 경우가 아니고 엔티티 객체나 VO(또는 Map)를 파라미터로 사용한다면 VO가 가진 모든 field 값을 키로 사용하는 경우는 많지 않을 것이다. 그럴 때는 아래와 같은 방법으로 키를 생성해서 사용할 수 있다.

    public class MemberController {
    
    	@PostMapping
    	@Cacheable(key = "T(com.example.springcache.util.KeyGen).generate(#member)", cacheNames = "Member")
    	public Member saveKeyGen(@RequestBody Member member){
    		log.info("save to cache :" + member);
    		return memberRepository.save(member);
    	}
    }
    
    // /util/KeyGen.java
    public class KeyGen {
    	public static Object generate(Member member) {
    		return member.getId() + ":" + member.getName();
    	}
    }

     

     

     

    Spring에서 Cache기능이 기본적으로 어떻게 동작하는지 알았으니 이제 Redis로 Cache 기능을 사용해보자.


    참고

    https://docs.spring.io/spring-framework/docs/3.1.0.M1/spring-framework-reference/html/cache.html

    https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache

    https://livenow14.tistory.com/56

    https://www.baeldung.com/spring-cache-tutorial

    http://dveamer.github.io/backend/SpringCacheable.html