작지만 꾸준한 반복

로컬 캐싱을 이용해서 성능을 높이자 본문

공부기록/Spring

로컬 캐싱을 이용해서 성능을 높이자

iamjooon2 2023. 11. 13. 01:37

집사의고민 프로젝트의 기능 개발은 완료되었고, 이제 리팩터링 및 성능 개선을 고민하고 있는 상황입니다.

그러던 중, 쉽게 바뀌지 않는 API를 캐싱하는 것은 어떨까라는 생각이 들었는데요.

 

적용 과정과 선택한 기술에 대한 근거를 정리하고자 합니다.

 

그 전에, 캐시는 무엇일까요

공대생의 친구, 위키백과에게 물어봤습니다

캐시(cache, 문화어: 캐쉬, 고속완충기, 고속완충기억기)는 컴퓨터 과학에서 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간없이 더 빠른 속도로 데이터에 접근할 수 있다.

 

라고 합니다.

 

제 언어로 정리해보자면, 미리 쟁여둔 값을 사용하는 것입니다.

풀링도 그렇고, 컴퓨터공학에서는 이렇게 쟁여두는 게 참 많은 것 같습니다

 

로컬 캐시?  글로벌 캐시?

캐시는 크게 로컬 캐시 글로벌 캐시 두 종류가 존재합니다

 

로컬 캐시는 서버의 내장 캐시를 사용하는 방법입니다. 로컬 서버의 메모리를 사용하여 캐싱처리를 하고,

따로 네트워크를 타지 않아도 된다는 장점이 있어요

 

글로벌 캐시는 여러 서버에서 캐시 서버에 접근하여 참조하는 캐시입니다

별도의 캐시 서버를 이용하여 서버간 데이터 공유가 쉽고, 네트워크 트래픽을 사용해야해서

로컬 캐시보다 속도는 느린 점이 있어요

 

저는 이 둘중 로컬 캐시를 선택했습니다.

크게 두가지 이유였는데요

 

첫번째로는, 글로벌 캐시를 사용하기에는 비용이 너무 크다고 생각했기 때문입니다

캐싱하려는 API는 두 개이고, 더 자세히는 ORM에서의 메서드 한 개입니다

이 메서드 하나를 위해 부가적인 인프라를 구축하는 것은 배보다 배꼽이 더 크다고 생각했어요

또한 현재 서버는 EC2 하나 내의 단일 WAS로 구성되어있습니다. 여러대의 서버로 scale-out되지 않은 상태라서, 여러 서버간의 정합성을 필요성도 없었습니다.

 

두번째로는, 로컬 캐시가 글로벌 캐시보다 더 빠르기 때문입니다

로컬캐시를 사용한다면 별도의 인프라에서 정보를 가져오기위한 네트워크 비용이 필요없기 때문입니다

 

사용 라이브러리

로컬 캐싱 라이브러리에 크게 Caffeine과 Ehcache 두 개가 대표주자격으로 있는 것 같더라구요

저는 Caffeine을 선택했습니다.

Ehcache는 xml 방식으로 적용해야해서 불편하고, Caffeine이 성능면에서 더 빠르더라구요

 

적용 방법

아래의 두 의존성을 추가했습니다

implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'

 

먼저 추가한 `spring-boot-starter-cache`는  스프링에서 cache를 구현할 수 있도록, 인터페이스를 제공합니다

 

`spring-boot-starter-web`에서 기본적으로 제공해주는 @EnableCaching이 있긴 하지만,

직접적으로 캐싱을 사용하기위한 기능은 포함되어 있지 않습니다

 

그리고 아래 caffeine은, 캐싱에 선택한 라이브러리입니다

 

코드

적용한 코드는 아래와 같습니다

 

@EnableCaching
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(caches());
        return cacheManager;
    }

    private List<CaffeineCache> caches() {
        return Arrays.stream(CacheType.values())
                .map(cacheType -> new CaffeineCache(cacheType.getName(), cache(cacheType)))
                .toList();
    }

    private Cache<Object, Object> cache(CacheType cacheType) {
        return Caffeine.newBuilder()
                .maximumSize(cacheType.getMaxSize())
                .build();
    }

}

@Getter
@AllArgsConstructor
public enum CacheType {

    BREEDS("breeds", 1),
    ;

    private final String name;
    private final int maxSize;

}

public interface BreedRepository extends JpaRepository<Breed, Long> {

    @Cacheable(cacheNames = "breeds", key = "'allBreeds'")
    default List<Breed> findAllBreeds() {
        return findAll();
    }
}

@Service
public class AdminService {

    @CacheEvict(cacheNames = "breeds", key = "'allBreeds'")
    public Long createBreed(Long petSizeId, String breedName) {
            PetSize petSize = petSizeRepository.getReferenceById(petSizeId);
            Breed breed = breedRepository.save(Breed.builder().name(breedName).petSize(petSize).build());
            return breed.getId();
    }

}

 

클래스 하나하나씩 설명해보자면,

 

먼저 CacheType은, 우리가 어플리케이션에서 임의로 등록해줄 캐시 타입입니다

이름과 최대 캐싱 개수를 Enum으로 만들어 담아두었고, 현재는 견종 목록만 만들어두었습니다

 

CacheConfig에서 이 Enum을 하나씩 말아올려,

캐시 매니저 빈을 등록하고, @EnableCaching을 통해 spring-mvc에서 캐싱을 적용하도록 해줍니다

 

캐시 적용할 메서드는 @Cacheable 어노테이션을 통해 적용해줄 수 있습니다.

위 어노테이션이 붙은 메서드가 처음 호출되고 난 뒤, 결과를 was단에서 캐싱해두어 다음 요청부터는 캐싱해둔 값이 나가게 됩니다

select * from breeds; 쿼리인 findAll() 메서드가 선언되어있는 Repository는 인터페이스기 때문에

default 접근제어자를 붙인 findAllBreeds() 메서드로 감싸주었습니다

 

캐시 미스는 @CacheEvict 어노테이션을 통해 적용해줄 수 있습니다

위 어노테이션이 붙은 메서드가 호출되면 같은 key를 가진 메서드들의 적용된 캐시가 무효화됩니다

(추가로, cacheNames는 앞서 적용해둔 임의의 cacheType의 설정값을 가져옵니다)

 

결과 

야무지게 캐싱을 적용해보았는데, 과연 성능상 이점은 생겼을까요?

 

 

로컬기준 각각

 

194ms -> 24ms

241ms -> 23ms

 

로 유의미한 차이를 확인할 수 있었습니다 :D

 

그렇다면 배포 환경에서는 어떤 차이가 있었을까요?

 

 

747ms -> 26ms

여러번 테스트 해본 결과 네트워크 환경 따라 20~40ms 왔다갔다 하는데요

약 2~30배로 성능을 개선할 수 있었습니다

 

나도 성능개선 해볼거야! 하면서 DB 인덱스 걸 것 찾아보고 그랬었는데요

성능개선의 가장 빠른 길은 그리 먼 곳에 있지 않았다는 사실을 새삼 깨닫게 되었습니다

드라마틱한 변화는 아니지만, 보는 시야가 넓어졌다고 생각하며 나아가보도록 하겠습니다

 

레퍼런스

https://loosie.tistory.com/806

https://velog.io/@injoon2019/Caffeine-Cache-%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

https://gngsn.tistory.com/170

우아한테크코스 5기 후추