부트캠프 과제 中 트러블슈팅 과정 작성
🛠️ 트러블 슈팅
0. 배경
사장이 어떤 가게를 소유하고 있는지 한번에 처리하기 위한 로직을 빼기 위해 고민했다.
Filter, Inteceptor 등 방법을 생각했지만, 다 포기하고 말았다.
사장이 소유하고 있는 가게를 확인하기 위해서는 결국 DB에 접근해야 하는데, 구현 로직이 아닌 완전 밖에 있는 filter나 interceptor에서 repository접근이 옳지 않아 보였다. 결국 공통으로 처리하는 걸 포기해야 하나 싶을 때 'Cache를 사용하라'는 조언을 들었다.
1. 문제
가게가 생성될 때 userId를 key로 캐시에 저장하고, 그 캐시는 List<Long>형태로 만들고 싶었다.
하지만 가장 첫단추인 캐시 저장부터 문제가 생겼다. 가게 생성 메서드 안에 있는 캐시 저장에 문제가 생기니, 가게가 제대로 생성조차 되지 않았다.
2. 원인
@Cacheable
로 캐시를 생성하는 글을 참고하며, 캐시를 생성하고 있으면 조회한다는 글을 보았다. 1명의 사장이 소유할 수 있는 가게는 최대 3개로, user_id를 이용해 저장된 값을 가져오고, 그 List에 add()
하는 방식을 사용했다.
결국 기존의 캐시를 수정하는 것이기 때문에 항상 메서드를 수행한 후 캐시에 저장하는 @CachePut
로 변경해보았다.
그래도 캐시에 store_id가 저장되지 않았다.
먼저 생성한 store를 db에 저장하고, cache에 저장하는 saveStoreToCache()
를 호출하고 있기 때문에 cache에서 문제가 생기면 createStore()
가 제대로 마무리 되지 않았다.
캐시를 반환하는 건 메서드의 반환 형태와 동일하다고 했기에 getCacheStore(userId)
를 가져왔었다. 그 후에 isEmpty()
로 확인을 했지만 이 부분이 첫번째 문제였다.
‘캐시에 값이 없으면 메서드를 호출한다~’는 내용을 제대로 이해하지 못해 발생한 문제였다. 캐시에 값이 없으면 메서드를 호출해 새로 생성할 줄 알았다. 캐시가 담길 공간이 자연스럽게 생성될 것이라 생각했지만 아니었다.
첫번째 가게를 생성할 때 getCacheStore()
를 하면 빈 리스트가 반환 된다. 그리고 이 비어있는 리스트 때문에 NPE가 발생하며 캐시에 값이 저장되지 않았다. 여기서 말하는 NPE가 그냥 cache가 null 이구나 라고 생각했는데 오히려 get~
으로 받아오면서 null에 접근하게 되는 것이었다.
3. 해결
getCacheStore()
를 통해 캐시를 받아오고, 그 뒤에 null인지 확인하는 게 아니라 그 순서를 바꿔야 했다.
수정 전
@Cacheable(key = "#userId", value = "myStores")
public List<Long> saveStoreToCache(Long userId, Long storeId) {
List<Long> cacheValue = myStoreCache.getCacheStore(userId);
if (cacheValue.isEmpty()) {
cacheValue = new ArrayList<>();
}
cacheValue.add(storeId);
return cacheValue;
}
수정 후
@CachePut(key = "#userId", value = "myStores")
public List<Long> saveStoreToCache(Long userId, Long storeId) {
List<Long> cacheValue = new ArrayList<>();
cacheValue.add(storeId);
return cacheValue;
}
수정 전에는 get~
으로 받아온 캐시가 empty라면 새로운 리스트를 생성했었고, 그 리스트 안에 값을 넣어준다는 단순한 생각으로 작성했다.
하지만 반대로 캐시가 들어갈 공간을 먼저 만들어주고 storeId를 저장해서 반환해준다. 그럼 캐시에는 storeId가 저장된다.
여기서 의문이 생겼다.
2개 이상의 가게를 생성했을 때, 로그를 찍어보면 여러 개의 가게가 조회 될까?
하지만 예상했던 결과와 달리, 처음 저장한 storeId만 로그에 찍혔다. 가게를 여러개 생성해도 처음 저장한 storeId만 보이고 있었다.
4. 의문점
설마 캐시에 제대로 저장이 되지 않는 걸까 싶었지만 다른 User를 생성해 접근하려고 하면 접근할 수 없다고 뜨며, 그 User가 새로운 Store를 생성하면 역시나 새로운 StoreId 하나만 로그에 찍히고 있다.
다른 User가 만든 곳에 접근 할 수 없으니 캐시를 통해서 제대로 확인을 해주고 있는 건 확실하다. 그런데 왜 처음 저장한 storeId만 로그에 찍히는 걸까?
그 문제는 getCacheStore()
에 있었다.
@Cacheable(key = "#userId", value = "myStores")
public List<Long> getCacheStore(Long userId) {
List<Long> stores = storeRepository.findStoreByUserId(userId);
if (stores.isEmpty()) {
throw new StoreNotFoundException();
}
return stores;
}
cache에 있는 값을 가져오는 getCacheStore()
구현을 이렇게 했다.
여기에 작성한 @Cacheable로 인해 캐시가 하나만 보였던 것이었다.
Cacheable
은 메서드 실행 전에, 캐시가 존재하면 캐시를 반환한다. 가게를 생성하고 저장했으니 캐시에 값은 들어가있다. 그래서 그 뒤에 가게를 더 생성하더라도 이미 존재하는 캐시만 반환하고 있던 것이었다.
그리고 처음 수정했던 saveStoreToCache()
에 작성한 @Cacheable을 @CachePut으로 변경했던 이유도 이미 캐시에 값이 있으면 그 값만 반환하기 때문에 변경했던 것. 캐시가 하나만 필요하면 상관없지만, 하나의 userId에 여러 storeId가 저장되어야 하므로 캐시도 업데이트가 필요했다. 메서드를 실행해야 하는 @CachePut
덕분에 새로운 캐시를 저장할 수 있었고, 변경된 캐시를 가져올 수 있다.
@CachePut(key = "#userId", value = "myStores")
public List<Long> getCacheStore(Long userId)
getCacheStore()
의 @Cacheable을 @CachePut로 변경하니 여러 개의 storeId가 리스트로 로깅된다.
덕분에 @Cacheable의 특징인 캐시가 존재하면 메서드 실행 전 반환을 이해할 수 있었다.
💡 느낀 점
처음 접해보는 캐시로 인해 어떻게 동작되는지 그 원리를 이해하기 힘들었다. 특히 메서드 반환으로 캐시가 저장된다는 것이 제일 어려웠다. 반환의 어떤 것이 저장된다는 거지? 그냥 반환 형태 그 자체로 저장되는 건가? 등등 직접 작성해보기 전까지는 설명만 적혀있는 걸 이해하지 못 했다.
코드를 직접 작성하더라도 어떻게 동작하는지 이해하지 못하고 참고 자료만 찾았기 때문에 오류 해결을 위해서 더 알아봐야 했다. 결국 오류를 해결하는 과정에서 @Cacheable과 @CachePut의 차이를 더 명확하게 알 수 있었다.
'Framework > SpringBoot' 카테고리의 다른 글
[Spring Boot] Toss Payments로 '가상계좌' 결제 기능 구현 (1) 연동하기 (0) | 2025.04.22 |
---|---|
[SpringBoot] Redis 적용 시 @Indexed 사용 문제점 (0) | 2025.03.26 |
[SpringBoot] 테스트 코드 작성 시 Mock과 Spy, 그리고 BCryptPasswordEncoder의 encode (0) | 2025.03.26 |
[SpringBoot] access token과 refresh token을 만들어 postman으로 테스트하기 (1) (0) | 2025.02.27 |
[SpringBoot] AOP를 이용해 Log를 남겨보자 -로깅 (0) | 2025.02.27 |