모르지 않다는 것은 아는것과 다르다.

Spring

캐시를 사용한 읽기 속도 최적화

채마스 2022. 3. 10. 23:31

Caffeine

  • 자바 8을 기반으로 개발된 고성능 캐시 라이브러리인 카페인을 사용해 보겠다.
  • 카페인은 자바의 표준 캐싱 API인 JCache 기반이다. 즉, JSR-107과 함께 스프링을 포함해서 다양한 자바 생태계의 프레임워크를 지원하고 있다.
implementation 'com.github.ben-manes.caffeine:caffeine:2.8.0'
  • 위와 같이 의존성을 추가해 준다.
@Test
void useCache() throws InterruptedException {
    // 캐시를 200ms 동안 유지하고, 최대 100개까지 저장해 둘 수 있는 카페인 캐시 객체 생성하기
    Cache<String, Object> cache = Caffeine.newBuilder()
            .expireAfterWrite(200, TimeUnit.MILLISECONDS)
            .maximumSize(100)
            .build();

    String key = "springrunner";
    Object value = new Object();        

    Assertions.assertNull(cache.getIfPresent(key));

    cache.put(key, value);        
    Assertions.assertEquals(value, cache.getIfPresent(key));

    TimeUnit.MILLISECONDS.sleep(100);
    Assertions.assertEquals(value, cache.getIfPresent(key));

    // 200ms 가 지났으므로 만료돼서 null 이 반환된다.
    TimeUnit.MILLISECONDS.sleep(100);
    Assertions.assertNull(cache.getIfPresent(key));
}
  • 하지만, 캐시를 사용할 때 주의할 점은 메모리의 크기가 무한하지 않다는 점이다.
  • 따라서 모든 객체를 메모리에 저장할 수는 없다.
  • 잘못사용하면 캐시로 사용하는 메모리의 크기가 너무 커서 애플리케이션이 사용해야 하는 메모리에 영향을 줄 수 있기 때문이다.
  • 따라서 캐시를 사용할 때에는 최대 개수라든가, 만료 시간과 같은 것들을 사용해서 효율적으로 캐시를 사용하는 것이 중요하다.
  • 위의 코드에서 expireAfterWrite, maximumSize 통해서 만료시간과 최대로 수용할 수 있는 객체의 수를 제한함으로써 메모리를 관리한다.

 

읽기 속도 최적화

  • 로컬에 데이터가 이미 있다면 원격지에 변경이 일어나지 않는 이상 새로 내려받지 않는식으로 최적화할 수 있다.
public class CsvMovieReader implements MovieReader {


    @Override
    public List<Movie> loadMovies() {

        try {
            final InputStream content = getMetadataResource().getInputStream();

            return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
                    .lines()
                    .skip(1)
                    .collect(Collectors.toList());
        } catch(IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }
    }

}
  • 위와 같은 코드를 아래와 같이 캐시를 적용시킬 수 있다.

 

public class CsvMovieReader implements MovieReader {

    private final Cache<String. List<Movie>> cache;

    public CsvMovieReader(Cache<String, List<Movie> cache>) {
        this.cache = Objects.requireNonNull(cache);
    }

    @Override
    public List<Movie> loadMovies() {

        /**
        * 캐시에 저장된 데이터가 있다면, 즉시 반환
        */
        List<Movie> movies = cache.getIfPresent("csv.movies");
        if (Objects.nonNull(movies) && movies.size() > 0){
            return movies;
        }    


        try {
            final InputStream content = getMetadataResource().getInputStream();

            return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
                    .lines()
                    .skip(1)
                    .collect(Collectors.toList());
        } catch(IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }

        /**
        * 획득한 데이터를 캐시에 저장하고, 반환한다.
        */
        cache.put("csv.movies", movies);
        return movies;
    }

}
@Bean
public CsvMovieReader csvMovieReader() {
    Cache<String, List<Movie>> cache = Caffeine.newBuilder()
                                                .expireAfterWrite(200, TimeUnit.MILLISECONDS)
                                                .maximumSize(100)
                                                .build();
}
  • 위와 같이 빈으로 CsvMovieReader 를 등록하면서 캐시의 기본 설정을 해줄 수 있다.
  • 위의 코드로 만족하는가? -> 만약 카페인이 아닌 다른 라이브러리를 사용하면 어떻게 되는가? -> 변경을 수용할 수 있도록 추상화 해야한다.
  • 캐시 매니저 인터페이스를 통해서 캐시를 생성하거, 구성하고, 사용을 관리할 수 있다.

 

public class CsvMovieReader implements MovieReader {

    private final CacheManager cacheManager;

    public CsvMovieReader(CacheManager cacheManager) {
        this.cacheManager = Objects.requireNonNull(cacheManager);
    }

    @Override
    public List<Movie> loadMovies() {

        Cache cache = cacheManager.getCache(getClass().getName());

        /**
        * 캐시에 저장된 데이터가 있다면, 즉시 반환
        */
        List<Movie> movies = cache.get("csv.movies", List.class);
        if (Objects.nonNull(movies) && movies.size() > 0){
            return movies;
        }    


        try {
            final InputStream content = getMetadataResource().getInputStream();

            return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
                    .lines()
                    .skip(1)
                    .collect(Collectors.toList());
        } catch(IOException error) {
            throw new ApplicationException("failed to load movies data.", error);
        }

        /**
        * 획득한 데이터를 캐시에 저장하고, 반환한다.
        */
        cache.put("csv.movies", movies);
        return movies;
    }

}
  • 위와 같이 CacheManager 를 통해서 캐시에 접근하는 식으로 코드를 수정한다.
@Bean
public CaffeineCacheManager caffeineCacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();
    cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS));

    return cacheManager;
}
  • 이렇게 카페인 매니저를 캐시 매니저로 설정해서 빈으로 등록한다.
  • 만약 카페인이 아니고 레디스를 사용하고 싶다면? -> 캐시 매니저를 레디스 매니저로만 바꿔주면 된다. -> 추상화가 적용되었다.




REFERENCES

  • 박용권님의 스프링러너 스프링 아카데미

'Spring' 카테고리의 다른 글

프락시 팩토리빈과 @AspectJ  (0) 2022.03.11
프로파일과 Resource 인터페이스  (0) 2022.03.10
자바코드로 의존관게 주입하기  (0) 2022.03.10
필터와 인터셉터  (0) 2022.02.27
파일 업로드  (0) 2022.02.27