마스터리북 시뮬레이터용 조회 API를 3개 만들어야 했습니다. 전체 마스터리북 조회, 타입별 조회(20, 30), 그리고 ID 기반 단건 조회였습니다. 데이터 변경이 거의 없는 정적 데이터라서 캐싱을 적용하기로 했습니다.
처음에는 각 API마다 개별적으로 캐싱하는 방식으로 구현했습니다. 전체 데이터 캐싱, 타입별 캐싱, 단건 캐싱을 따로따로 하는 방식이었죠. 하지만 코드를 다 작성하고 산책을 하다보니 "이게 불필요하게 복잡한 것 같은데?"라는 생각이 들었습니다.
전체 데이터를 기반으로 각각의 캐시를 만드는 게 더 효율적일 것 같았습니다. 한 번의 DB 조회로 모든 캐시를 채울 수 있다면 훨씬 나을 텐데 말이죠.
아래 코드, 엔티티명, API 경로, 데이터 값은 모두 임의의 예시이며 실제 서비스와 무관합니다.
처음에는 각 API에 대해 독립적인 캐싱을 구현했습니다.
@Service
public class ItemCacheService {
@Cacheable(value = "all_items")
public ItemListResponse findAllItems() {
return itemService.findAllItems();
}
@Cacheable(value = "items_by_type", key = "#itemType")
public ItemListResponse findItemsByType(Integer itemType) {
return itemService.findItemsByType(itemType);
}
@Cacheable(value = "item_by_id", key = "#id")
public Optional<ItemResponse> findItemById(Integer id) {
return itemService.findItemById(id);
}
}
이 방식의 문제는 각 메서드가 독립적으로 DB에 접근한다는 것이었습니다.
서버 시작 후 사용자가 다음과 같은 순서로 API를 호출한다면:
총 3번의 DB 쿼리가 발생했습니다. 같은 데이터를 조회하는데 말이죠.
문제를 인식하고 나서 "전체 데이터를 한 번 조회해서 메모리에서 필터링하자"는 아이디어를 떠올렸습니다. 그래서 다음과 같이 구현했습니다.
@Service
public class ItemCacheService {
private final CacheManager cacheManager;
@CachePut(value = "all_items")
public ItemListResponse refreshAllItemsCache() {
ItemListResponse response = itemService.findAllItems();
// 전체 데이터에서 타입별로 분리하여 캐시 저장
saveItemsByTypeFromAllData(response, 20);
saveItemsByTypeFromAllData(response, 30);
// 전체 데이터에서 단건 캐시도 함께 저장
saveItemsByIdFromAllData(response);
return response;
}
private void saveItemsByTypeFromAllData(ItemListResponse allData, Integer itemType) {
List<ItemResponse> filtered = allData.getItems().stream()
.filter(item -> itemType.equals(item.getItemType()))
.toList();
Cache cache = cacheManager.getCache("items_by_type");
cache.put(itemType, ItemListResponse.builder().items(filtered).build());
}
private void saveItemsByIdFromAllData(ItemListResponse allData) {
Cache cache = cacheManager.getCache("item_by_id");
allData.getItems().forEach(item -> {
cache.put(item.getId(), Optional.of(item));
});
}
}
성과는 있었지만...
이 방식으로 DB 쿼리를 3번에서 1번으로 줄일 수 있었습니다. 하지만 코드가 상당히 복잡해졌습니다.
캐시 매니저를 직접 다루고, 수동으로 각 캐시에 데이터를 넣는 로직들이 추가되면서 말이죠.
"잠깐, 이게 맞나? 더 간단한 방법이 있을 것 같은데..."
코드를 다 작성하고 나서 산책을 나갔습니다. 걸으면서 방금 짠 코드를 떠올려보니 뭔가 이상했습니다.
생각해보니 제가 해결하려던 핵심 문제는 "같은 데이터를 여러 번 조회하는 것"이었습니다. 그런데 해결책이 오히려 복잡해졌죠. 캐시 매니저를 직접 조작하고, 수동으로 각 캐시에 데이터를 넣고, 예외 처리도 따로 해야 하고...
그때 이런 생각이 들었습니다:
복잡한 최적화보다는 단순하고 명확한 해결책이 더 좋을 수도 있겠다는 생각이 들었습니다. 특히 마스터리북 같은 정적 데이터는 말이죠.
집에 돌아와서 다시 생각해보니 답은 의외로 단순했습니다. 굳이 복잡한 캐시 조작 없이도 Spring Cache의 기본 기능만으로 충분했죠.
@Service
@Slf4j
public class ItemCacheService {
private final ItemService itemService;
// 서버 시작 시 전체 데이터 캐싱
@PostConstruct
public void initializeCache() {
log.info("Cache initialization started");
try {
findAllItemsCache();
log.info("Cache initialization completed");
} catch (Exception e) {
log.error("Cache initialization failed", e);
}
}
@Cacheable(value = "items")
public ItemListResponse findAllItemsCache() {
return itemService.findAllItems();
}
// 전체 캐시에서 타입별 필터링
public ItemListResponse findItemsByTypeCache(Integer type) {
ItemListResponse allItems = findAllItemsCache();
List<ItemResponse> filtered = allItems.getItems().stream()
.filter(item -> type.equals(item.getType()))
.toList();
return ItemListResponse.builder()
.items(filtered)
.build();
}
// 전체 캐시에서 단건 검색
public Optional<ItemResponse> findItemByIdCache(Integer id) {
ItemListResponse allItems = findAllItemsCache();
return allItems.getItems().stream()
.filter(item -> id.equals(item.getId()))
.findFirst();
}
@CachePut(value = "items")
public ItemListResponse refreshAllItemsCache() {
return itemService.findAllItems();
}
}
이번 경험을 통해 몇 가지 중요한 교훈을 얻었습니다.
1. 문제의 본질 파악하기
처음에는 "각각 캐싱해야 한다"고 생각했지만, 실제 문제는 "중복 DB 조회"였습니다. 문제를 정확히 파악했다면 처음부터 더 단순한 해결책을 찾을 수 있었을 것입니다.
2. 단순함의 가치
복잡한 최적화가 항상 좋은 것은 아닙니다. 특히 데이터 크기가 크지 않은 경우, 메모리 필터링만으로도 충분한 성능을 얻을 수 있습니다. 복잡성은 버그의 온상이 되기 쉽죠.
3. 한 발 물러서서 생각하기
코딩에 몰입하다 보면 터널 비전에 빠지기 쉽습니다. 산책처럼 잠시 코드에서 벗어나는 시간이 오히려 더 나은 해결책을 찾게 해줍니다.
결국 가장 좋은 코드는 동작하면서도 이해하기 쉬운 코드입니다. 성능 최적화도 중요하지만, 그 과정에서 코드가 불필요하게 복잡해진다면 다른 방법을 고민해볼 필요가 있습니다.
여러분도 비슷한 상황에서 "더 간단한 방법이 있을 것 같은데?"라는 생각이 든다면, 잠시 코드에서 벗어나 다른 관점에서 문제를 바라보시기 바랍니다.😂
시리즈 잘 봤습니다. 하나의 글을 읽다보니 관심이 생겨 시리즈 모든 글을 읽게 되었네요.
게이머분들에게 꼭 필요한 플랫폼을 개발하시는 걸 보니 정말 멋있다고 느꼈습니다!
항상 건승하세요