본문 바로가기
공부/Cache

카페인 캐시를 적용해보자.

by 고구밍 2022. 5. 28.

 

참고한 링크

더보기

결정적으로 참고한 코드)

https://velog.io/@_koiil/Caffeine

 

[Spring] Caffeine

이름부터 귀엽다 ☕️

velog.io

결정적으로 변형한 코드)

https://github.com/eugenp/tutorials/blob/master/spring-caching/src/main/java/com/baeldung/caching/example/AbstractService.java

 

GitHub - eugenp/tutorials: Just Announced - "Learn Spring Security OAuth":

Just Announced - "Learn Spring Security OAuth": . Contribute to eugenp/tutorials development by creating an account on GitHub.

github.com

-> 캐시로 담기위해서는 파라미터로 받는 정보에서 key값을 정해줘야 합니다.

// 성훈 - 거래내역서 보기
@Cacheable(cacheNames = "barterMyInfo", key = "#userDetails.userId")
public List<BarterDto> showMyBarter(UserDetailsImpl userDetails) {
    User user = userRepository.findById(userDetails.getUserId()).orElseThrow(
            () -> new CustomException(NOT_FOUND_USER)

 

결정적으로 참고한 자료)

https://stackoverflow.com/questions/69853693/caffeine-cache-with-spring-boot-not-working

 

Caffeine cache with spring boot not working

I've set up a scenario using caffeine cache and I can't get it working, the real method is always called when the parameters are the same. Here is my config: pom.xml ... <dependency> <

stackoverflow.com

The @Cacheable method has to be located inside a @Bean, @Component, @Service...

-> 컨트롤러에서는 캐쉬를 사용하지 않는다. (GET맵에서는 X)

 

 

참고한 사이트들)

캐시사용법 추천)

https://livenow14.tistory.com/56

 

[SpringBoot] Local-Memory 캐시를 사용해보자

@Service public class PathService { private static final Logger logger = LoggerFactory.getLogger(PathService.class); private final LineService lineService; private final StationService stationServic..

livenow14.tistory.com

https://www.baeldung.com/java-caching-caffeine

 

Introduction to Caffeine | Baeldung

Learn how to use the high-performing Caffeine caching library for Java.

www.baeldung.com

https://gngsn.tistory.com/157

 

Spring Cache, 제대로 사용하기

Spring Cache 사용법, Annotation 등을 알아보고 설정 방식을 알아보는 것이 해당 포스팅의 목표입니다. 해당 포스팅에서는 Spring Cache에 대해 다룹니다. Cache를 사용하기 위해서는 CacheManager가 필요한데

gngsn.tistory.com

https://blog.yevgnenll.me/posts/spring-boot-with-caffeine-cache

 

Spring boot 에 caffeine 캐시를 적용해보자 - 어떻게하면 일을 안 할까?

부제: 어떻게 하면 일을 조금이라도 안할까?

blog.yevgnenll.me

https://velog.io/@qotndus43/Cache

 

스프링부트 Caching 도입하기(Redis, Ehcache)

가져오는데 비용이 드는 데이터를 한번 가져온 뒤에는 복사본을 임시로 저장해둠으로써 애플리케이션 처리속도를 높이는 방식을 의미합니다.그렇다면 캐시는 언제 어디서 사용될까요?캐시는

velog.io

https://livenow14.tistory.com/56

 

[SpringBoot] Local-Memory 캐시를 사용해보자

@Service public class PathService { private static final Logger logger = LoggerFactory.getLogger(PathService.class); private final LineService lineService; private final StationService stationServic..

livenow14.tistory.com

https://sunitc.dev/2020/08/27/springboot-implement-caffeine-cache/

 

SpringBoot: Implement caching with Caffeine.

In this blog we will look into how to use Spring’s caching framework to add basic caching support to any Spring Boot application, and also look at some issues with caching if not implemented …

sunitc.dev

https://dpdpwl.tistory.com/81

 

[Java]자바 스트림Stream(map,filter,sorted / collect,foreach)

자바8부터 Stream 을 사용 할 수 있습니다. 기존에 자바 컬렉션이나 배열의 원소를 가공할떄, for문, foreach 등으로 원소 하나씩 골라내여 가공을 하였다면, Stream 을 이용하여 람다함수형식으로 간결

dpdpwl.tistory.com

 

 

 

코드에 적용하자!

1. Application

더보기

 

Application에 @EnableCaching을 추가해서 캐시를 활성화 시킵니다.

위 링크에서는 config라는 파일을 따로 만들었지만, Bean을 어플리케이션에 바로 넣는 것이 좋을 것 같다고,

팀장님이 말씀해 주셨습니다. 

 

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
public class MulmulApplication {

    @PostConstruct
    public void started(){
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
    }

    public static void main(String[] args) {
        new SpringApplicationBuilder(MulmulApplication.class)
                .run(args);
    }

    @Bean
    public PageableHandlerMethodArgumentResolverCustomizer customize() {
        return p -> p.setOneIndexedParameters(true);
    }

    @PersistenceContext
    EntityManager em;

    static { System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true"); }

    @Bean
    public CacheManager cacheManager() {
        List<CaffeineCache> caches = Arrays.stream(CacheType.values())
                .map(cache -> new CaffeineCache(cache.getCacheName(), Caffeine.newBuilder().recordStats()
                                .expireAfterWrite(cache.getExpiredAfterWrite(), TimeUnit.SECONDS) // 초,분,시,일...등 가능
                                .maximumSize(cache.getMaximumSize())
                                .build()
                        )
                )
                .collect(Collectors.toList());
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        simpleCacheManager.setCaches(caches);
        return simpleCacheManager;
    }

}

 

2. CacheType

더보기

 

enum형태의 CacheType을 추가하였습니다.

위 어플리케이션에서 CachName, expireAfterWrite(유효기간), maximumSize (저장할 캐시의 갯수)을 설정합니다.

여기서 expireAfterWrite의 단위를 second단위로 하였습니다.

 


 

카페인 : 로컬캐시를 저장할 떄 -> 잘 변하지 않는 정보를 조회할 때
사용하는 것을 추천한다고 하는 것 같네요.저희 서비스에서는

 

상대적으로 다른 캐시보다 설정하기가 쉽고

aws서버를 1개로 돌리고 있기 때문에 전체에 대해서 카페인 캐시를 적용해도 된다고 판단하였습니다.

 

거래내역 > 아이템 > 유저정보


순으로 데이터가 자주 변하는 것 같다는 생각하였습니다. 

 

package com.sparta.mulmul.model;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum CacheType {

    USER_PROFILE("userProfile", 30, 10000),
    USER_BAN("userBan", 10, 10000),
    USER_SCORE("userScore", 10, 10000),
    ANOTHER_USER_PROFILE("anotherUserProfile", 30, 10000),


    CHAT_INFO("chatInfo", 1, 10000),
    CHAT_LIST_INFO("chatListInfo", 2, 10000),
    NOTIFICATION_INFO("notificationInfo", 5, 10000),

    ITEM_INFO("itemInfo", 10, 10000),
    ITEM_DETAIL_INFO("itemDetailInfo", 10, 10000),
    HOT_ITEM_INFO("hotItemInfo", 10, 10000),
    ITEM_SEARCH_INFO("itemSearchInfo", 5, 10000),
    ITEM_TRADE_INFO("itemTradeInfo", 10, 10000),
    ITEM_TRADE_CHECK_INFO("itemTradeCheckInfo", 10, 10000),
    SCRAB_ITEM_INFO("scrabItemInfo", 10, 10000),


    BARTER_My_INFO("barterMyInfo", 5, 10000),
    BARTER_INFO("barterInfo", 5, 10000);



    // 캐시 이름, 만료 시간, 저장 가능한 최대 갯수
    private final String cacheName;
    private final int expiredAfterWrite;
    private final int maximumSize;

}

 

3. Service에 적용하기

더보기

 캐시 작동하는 방법)

 

캐시 작동방식에 대한 설명

https://velog.io/@qotndus43/Cache

 

스프링부트 Caching 도입하기(Redis, Ehcache)

가져오는데 비용이 드는 데이터를 한번 가져온 뒤에는 복사본을 임시로 저장해둠으로써 애플리케이션 처리속도를 높이는 방식을 의미합니다.그렇다면 캐시는 언제 어디서 사용될까요?캐시는

velog.io

 

 2일동안 개인적으로 가장 고생했던 부분입니다.

1. @Cacheable과 캐시이름을 붙여줍니다.

@Cacheable은 value와 key를 같이 사용하여 캐시의 키값으로 사용 -map

 

2. 캐싱된 데이터가 있으면 메서드를 실행없이 데이터를 반환하고,

없으면 DB에서 조회한 다음 메서드 return값을 캐시합니다.

 

 

 

3. 캐시를 작동하기위해서 key값이 필요한데,

사용자가 key값을 설정하지 않으면 자동으로 파라미터로 key값으로 준다고 합니다.

 

key값에 대한 설명

https://livenow14.tistory.com/56

 

[SpringBoot] Local-Memory 캐시를 사용해보자

@Service public class PathService { private static final Logger logger = LoggerFactory.getLogger(PathService.class); private final LineService lineService; private final StationService stationServic..

livenow14.tistory.com

결정적으로 참고한 코드 - 카페인캐시 튜토리얼 코드 

https://github.com/eugenp/tutorials/blob/master/spring-caching/src/main/java/com/baeldung/caching/example/AbstractService.java

 

GitHub - eugenp/tutorials: Just Announced - "Learn Spring Security OAuth":

Just Announced - "Learn Spring Security OAuth": . Contribute to eugenp/tutorials development by creating an account on GitHub.

github.com

 

위 코드를 보고 

파라미터를 기준으로 key값을 저장한다고 해서 유저디테일의 유저아이디를 key값으로 설정하였습니다.

 

1. 파라미터에 userDetails가 주어졌음

-> key값에는 숫자형태로 오는 것이 아니면 자동으로 인식하지 못한다는 것 같습니다.

-> 따라서 key = "" 형식으로 key값을 정해줘야하는데,

userDetails(유저토큰에서 받은 유저 정보)에 있는 고유값은

username(중복되지 않는 email), userId가있지만,

 

테스트 당시 db에 수동 기입을 하여서 email을 null로 설정했기 때문에 저는 userId를 key값으로 설정하였습니다.

-> String형식의 고유한 key값이면 상관 없을 것 같습니다.

 

 

 // 성훈 - 거래내역서 보기
    @Cacheable(cacheNames = "barterMyInfo", key = "#userDetails.userId")
    public List<BarterDto> showMyBarter(UserDetailsImpl userDetails) {
        User user = userRepository.findById(userDetails.getUserId()).orElseThrow(
                () -> new CustomException(NOT_FOUND_USER)
        );

        Long userId = userDetails.getUserId();
        // 유저의 거래내역 리스트를 전부 조회한다
        List<Barter> mybarterList = barterRepository.findAllByBuyerIdOrSellerId(userId, userId);
        // 거래내역 리스트를 담기
        List<BarterDto> totalList = addTotalList(userId, mybarterList);
        return totalList;
    }

 

아래와 같이 파라미터로 Long타입의 숫자를 받게된다면

@Cacheable이 자동으로 파라미터 값을 Key값으로 설정하기 때문에 바로 작동이 됩니다.

 

// 이승재 / 유저 스토어 목록 보기
@Cacheable(cacheNames = "anotherUserProfile")
public UserStoreResponseDto showStore(Long userId) {
    User user = userRepository.findById(userId).orElseThrow(
            () -> new IllegalArgumentException("유저 정보가 없습니다.")
    );
    String nickname = user.getNickname();
    String profile = user.getProfile();
    float grade = user.getGrade();
    String degree = user.getDegree();
    String address = user.getAddress();
    String storeInfo = user.getStoreInfo();

    Long userBadId = bagRepository.findByUserId(userId).getId();
    List<Item> myItemList = itemRepository.findAllByBagId(userBadId);
    List<ItemUserResponseDto> itemUserResponseDtos = new ArrayList<>();

    for (Item item : myItemList) {
        Long itemId = item.getId();
        String itemImg = item.getItemImg().split(",")[0];
        int status = item.getStatus();
        ItemUserResponseDto itemUserResponseDto = new ItemUserResponseDto(itemId, itemImg, status);
        itemUserResponseDtos.add(itemUserResponseDto);
    }

    return new UserStoreResponseDto(nickname, profile, degree, grade, address, storeInfo, itemUserResponseDtos);

}

 

특별한 파라미터가 없었지만, 잘 작도 되었습니다.

 

 @Cacheable(cacheNames = "hotItemInfo")
    public List<ItemStarDto> hotItem() {
        int status = 1;
//        List<Barter> barterList = barterRepository.findAllByBarter(status);
        List<HotBarterDto> barterDtoList = barterRepository.findByHotBarter(status);
        List<ItemStarDto> itemStarDtoList = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        searchSellerItem(barterDtoList, map);

        List<String> listKeySet = new ArrayList<>(map.keySet());
        // 내림차순
        Collections.sort(listKeySet, (value1, value2) -> (map.get(value2).compareTo(map.get(value1))));

        int cnt = 0;
        brackTopThree(itemStarDtoList, map, cnt, listKeySet);
        return itemStarDtoList;
    }

    private void searchSellerItem(List<HotBarterDto> barterList, Map<String, Integer> map) {
        for (HotBarterDto eachBarter : barterList) {
            String sellerItem = eachBarter.getBarter().split(";")[1];
            Integer count = map.get(sellerItem);
            if (count == null) {
                map.put(sellerItem, 1);
            } else {
                map.put(sellerItem, count + 1);
            }
        }
    }

    private void brackTopThree(List<ItemStarDto> itemStarDtoList, Map<String, Integer> map, int cnt, List<String> listKeySet) {
        for (String key : listKeySet) {
//            System.out.println("key : " + key + " , " + "value : " + map.get(key));
            Long sellerItemId = Long.parseLong(key);
            BarterHotItemListDto sellerItem = itemRepository.findByHotBarterItems(sellerItemId);
            if (sellerItem.getStatus() == 0 || sellerItem.getStatus() == 1) {
                ItemStarDto itemStar = new ItemStarDto(
                        sellerItem.getItemId(),
                        sellerItem.getItemImg().split(",")[0],
                        sellerItem.getTitle(),
                        sellerItem.getContents()
                );
                itemStarDtoList.add(itemStar);
                cnt++;
            }
            if (cnt == 3) {
                break;
            }
        }
    }

 

잘 작동이 된다 == ( " 쿼리가 발생하지 않고, 캐시에 저장되어 있는 값을 반환해준다. " )

 

BARTER)
거래내역 보기 : 5초 

 

CHAT)
전체 채팅방 목록 가져오기 : 2초
개별 채팅방 메시지 불러오기 : 1초
차단 유저 목록 보기 : 10초

-> 다른 채팅과 관련된 GET은 단순 조회가 아닌 것 같아서 캐시 적용 X 

 

NOTIPICATION)

알림 전체 목록 : 5초

 

ITEM)

아이템 전체조회(카테고리별) : 10초
아이템 상세페이지 : 10초
아이템 검색 : 5

교환신청하기 전 정보 : 10초

 

USER)
마이페이지 내 정보 보기 : 30초
찜한 아이템 보여주기 : 10초
유저 스토어 목록 보기 : 10초
상대 정보 보여주기 : 10초
교환신청 확인 페이지 : 10초

 

 

 

5조 JMETER 부하테스트.xlsx
0.11MB

 

그렇게하여서 다음과 같은 결과를 얻었습니다.

캐시를 적용한 이후 JMETER테스트를 통해서

<처리량 & 수신 & 전송> 값이 DSL2차에비해 7배가 늘어난 것을 확인 할 수 있었습니다.

정확한 서버의 응답과정에대해서 깊게 생각해 보진 않았지만,

 

 

이번 캐시 적용을 통해서 캐시가 주는 성능향상을 새롭게 알게되었습니다.

 

블로그 글에서 읽었던 이야기 중에서,

"시니어 개발자가 자주바뀌는 자료는 조심해서 캐시를 써야한다고 하시면서,아주 짧게라도 캐시를 덕지덕지 붙인다."

라는 말의 의미를 이해할 수 있었습니다.

 

시간대별로 송수신 상태가 달라서 믿을 수 잇는 자료인지는 잘모르지만,단순히 비슷하게 숫자를 맞춰서

평균 시간을 계산해 보자면 캐시를 적용한 속도는 dsl2차와 전송속도를 비슷하게 보정을 하여서

 

평균속도에 * 2해서 비교해보면

100-(4946.5*2/13880.6*100) = 약 28.72%로

28%정도 빨라졌네요

 

아직 수신, 전송과 관련한 수치와 측정 시간에 따른 성능 변화의 문제로 단정 지을 수 없지만,

성능이 개선이 된것은 확실 한 것 같습니다.

 

수신과 전송이 둘다 7배 늘어버렸네요...뭐지
 
평균속도는 송수신값과 동일하게 놓았을경우 송수신 차이가 7배 차이이므로

100 - (5000*7 / 58696<- DSL평균속도  * 100<- 백분율) 해서 DSL에비해서 40% 빨라 졌네요.
그리고 오류도 2.53% -> 0.88%으로 줄었습니다

극단적으로 측정했엇던 2만 6천회 조회했을 때 결과입니다.
위와 같은 방법으로 계산해보면 평균속도는 DSL에비해서 13% 단축하였고,
오류는 58.5% -> 38.6% 으로 34% 감소하였습니다.

 

근데, 뭔가 캐시처리해서그런지 송수신 비율로 평균속도를 보정한다는게 이상하긴하네요
JPA랑 비교했을 경우 오히려 느린걸로 되서, 그리 정확한 방법은 아닌 것 같아요
 
 
 
장난삼아서 계산해보았는데,
거래내역에서 2.6만번 조회했을 때 단순 비교로 평균속도가
 
JPA 93080.5 기준으로 (93초 : 1분 33초)
 
DSL (DTO로 조회했을 경우) 61426.3 -> 32.96% (1분 2초)
캐시 (DSL + 캐시) 7686.4 -> 96.78% (7.6초)
으로 단축한거네요

 

'공부 > Cache' 카테고리의 다른 글

캐시 KEY값 고도화하기 - 1  (0) 2022.05.30
캐시를 알아보자 - 2  (0) 2022.05.27
카페인 로컬 캐쉬를 알아보자 -1  (0) 2022.05.27