본문 바로가기
프로젝트/미니

효과적인 부분 문자열 검색(Full-Text, 캐시, Redis)

by HWK 2023. 9. 23.

%Like%의 성능 관련 이슈에 대해서 보았다.
%Like% 쿼리는 데이터베이스의 모든 레코드를 스캔하며 부분 문자열을 찾기 위해 일치하는 모든 데이터를 찾습니다. 이로 인해 대량의 데이터가 있는 테이블에서는 성능 문제가 발생할 수 있으며 쿼리 실행 시간이 길어질 수 있습니다.
라고한다.
고로 어찌해야 성능 관련 이슈를 해결할 수 있을까라는 생각을 해봤다.
생각을 해봤자, 인간으로써 빠르게 찾는 방법은 딱히 생각나지 않았고,
검색결과 Full-Text와 캐시를 이용하는 방법을 찾았다.

 

Full-Text

Full-Text는 빠르기보다는 정확한 일치를 확인하는 것이다. 이는 자연어를 이용하여 데이터를 검색할 수 있도록 모든 데이터의 문자열 단어를 저장한다.
고로 이렇게 문자열 단어를 저장하면 인덱싱 방식을 활용할 수 있다.
하지만 Full-Text는 빠르지 않고 오히려 Contains보다 느릴것이다.
정확하긴 하지만 굳이 배달앱에서 그정도 정확성을 요구하지는 않을것이니 다음에 구현해보도록 하자.

 

캐시

자주 사용하는 데이터를 미리 보관해둔 임시 장소를 의미한다. 저장공간은 작고 비용이 비싸다.
주로 변경될 일이 없는 데이터베이스 조회 값을 캐시로 사용하는게 좋다.
또한 자주 호출되는 데이터를 캐시에 저장하는 것이 좋다.

스프링 부트에 사용하는 캐시는 대부분 JSR-107을 따른다.

캐시에는 로컬 캐시와 글로벌 캐시가 있다.

  • 로컬 캐시
    로컬에서만 사용하는 캐시이다.
    외부 서버와 트랜잭션 비용이 들지 않기 때문에 속도가 빠르다.
    하지만 분산 서버 구조에서 캐시를 공유하기는 어렵다.
  • 글로벌 캐시
    여러 서버에서 접근할 수 있다. 서버간 데이터 공유하기에 좋다.
    하지만 좀 느리다.

 

 

Redis

Remote Dictionary Server의 줄임말인 Redis는 모든 데이터를 메모리에 저장하고 조회하는 인메모리 데이터베이스, 메모리 기반의 key-value 구조의 데이터 관리 시스템이다. 오픈소스 데이터 관리 시스탬이며 가장 범용정으로 사용된다.

레디스의 특징은 모든 데이터를 메모리에 저장하고 조회하기 때문에
빠른 Read, Write 
속도를 보장하고  다양한 자료구조를 지원한다는 점이다.

 

Redis가 지원하는 데이터 형식은 다음과 같다.
String, Set, Sorted, Set, Hash, List

 

어떤 데이터를 정렬을 해야하는 상황이 있을 때 DBMS를 이용한다면
DB에 데이터를 저장하고 -> 저장된 데이터를 정렬하여 -> 다시 읽어오는 과정은 디스크에 직접 접근을 해야하기 때문에
시간이 더 걸린다는 단점이 있다. 이 때 In Memory 데이터베이스인 Redis를 이용하고
레디스에서 제공하는 Sorted Set이라는 자료구조를 사용하면 더 빠르고 간단하게 데이터를 정렬할 수 있다.
여러개의 복제본을 만들 수 있으며, 오랜 시간동안 고장나지 않는다.

또한 Redis는 확장성이 좋다.
Redis는 싱글 스레드이다.
1번에 1개의 명령어만 실행할 수 있다. (자주 비교되는 맴캐쉬드는 멀티 스레드 지원)

Keys(저장된 모든키를 보여주는 명령어)나 flushall(모든 데이터 삭제)등의 명령어를 사용할 때, 맴캐쉬드의 경우 1ms정도 소요되지만 레디스의 경우 100만건의 데이터 기준 1초로 엄청난 속도 차이가 있다.

즉 하나의 요청이 병목되면 그 다음 요청들이 계속 밀리기 때문에 O(N)관련 명령어를 주의해야한다. O(N)관련 명령어로는 위에서 언급한 Keys, flushall를 포함해 FLUSHDB, Delete COlLECTIONS, Get All Collections가 있다. 큰 컬렉션의 데이터를 다 가져오는 경우 등도 주의하자.

또 RDB 작업(특정 간격마다 모든 데이터를 디스크에 저장)이 매우 오래걸린다. AWS 60기가 메모리 기준으로 10분이나 소요된다고 한다. Redis 장애에 원인의 대부분이 해당 기능 때문에 발생하기 때문에 사용할 때 주의해야한다.
참고자료: https://sudo-minz.tistory.com/101

 

Redis 레디스 특징, 장단점, Memcached와 redis 비교

Redis 레디스 특징, 장단점, Memcached와 redis 비교 Redis(Remote Dictionary Storage, 레디스)는 모든 데이터를 메모리에 저장하고 조회하는 인메모리 데이터베이스, 메모리 기반의 key-value 구조의 데이터 관리

sudo-minz.tistory.com


일단 레디스와 상관 없이 캐시부터 이용해봤다.
다음과 같이 Service 코드를 바꿔주었고, springFramework.cache 어노테이션을 이용했다.
그 결과 다음과 같은 코드가 나왔다.

@Service
@RequiredArgsConstructor
public class StoreService {

    private final StoreRepository storeRepository;
    private final DeliveryRepository deliveryRepository;
//    @Autowired
//    private RedisTemplate<String, Object> redisTemplate;

    @CacheEvict(value = "storeCache", allEntries = true)
    public StoreResponseDto createStore(StoreRequestDto requestDto, UserDetailsImpl userDetails) {
        // user 정보 userDetails에서 추출
        User user = userDetails.getUser();

        // requestDto 정보를 저장
        Store store = new Store(requestDto, user);

        // store 정보를 repository에 저장
        Store saveStore = storeRepository.save(store);

        // store 정보를 DTO에 넣어 반환
        StoreResponseDto storeResponseDto = new StoreResponseDto(saveStore);
        return storeResponseDto;
    }

    public List<StoreResponseDto> getStores() {
        return storeRepository.findAllByOrderByStorePointDesc().stream().map(StoreResponseDto::new).toList();
//        return storeRepository.findAll().stream().map(StoreResponseDto::new).toList();
    }

    @Cacheable(value = "storeCache", key = "#keyword")
    public List<StoreResponseDto> getStoreByKeyword(String keyword) {
        List<StoreResponseDto> results = storeRepository.findAllByStoreNameContaining(keyword)
                .stream().map(StoreResponseDto::new).toList();
//        // Redis 캐시에 TTL 설정 (예: 30초)
//        redisTemplate.expire("storeCache::" + keyword, 30, TimeUnit.SECONDS);
        return results;
    }

    @CacheEvict(value = "storeCache", key = "#keyword")
    public void clearStoreCache(String keyword) {
        // 특정 키워드의 캐시를 지움
    }

    @CacheEvict(value = "storeCache", allEntries = true)
    public void clearAllStoreCaches() {
    }

    @Transactional
    @CacheEvict(value = "storeCache", allEntries = true)
    public StoreResponseDto updateStore(Long id, StoreRequestDto requestDto) {
        Store store = findStore(id);
        store.update(requestDto);
        return new StoreResponseDto(store);
    }

    @CacheEvict(value = "storeCache", allEntries = true)
    public MessageResponseDto deleteStore(Long id) {
        Store store = findStore(id);
        storeRepository.delete(store);
        MessageResponseDto messageResponseDto = new MessageResponseDto(
                "업장 삭제가 완료되었습니다.", 200
        );
        return messageResponseDto;
    }

    @Transactional
    public MessageResponseDto deliveryDone(Long orderId, User user) {
        Delivery delivery = deliveryRepository.findById(orderId).orElseThrow(()->
                new IllegalArgumentException("유효하지 않은 주문ID 입니다.")
        );
        Store store = delivery.getStore();
        if(store.getUser().getId() != user.getId())
            throw new IllegalArgumentException("주문을 받은 가게의 주인만 배달 완료를 진행할 수 있습니다.");
        delivery.deliveryDone();
        return new MessageResponseDto("배달이 완료되었습니다!", 200);
    }

    public List<OrderResponseDto> deliveryCheck(Long storeId) {
        Store store = findStore(storeId);
        List<OrderResponseDto> responseDtos = deliveryRepository.findAllByStore(store).stream().map(OrderResponseDto::new).collect(Collectors.toList());
        return responseDtos;
    }

    private Store findStore(Long id) {
        return storeRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("데이터가 없습니다.")
        );
    }

    public Long getStoreId(UserDetailsImpl userDetails) {
        return storeRepository.findStoreByUser(userDetails.getUser()).getId();
    }
}

@Cacheable는 key-value 형태로 캐시를 저장해주고,
@CacheEvict는 캐시를 삭제해준다. key를 지정할 수도 있고, 모든 캐시를 지울 수도 있다.
더이상 같은 검색 결과에 대한 쿼리가 수행될 필요가 없었고, 다른 곳에도 이용해야겠다는 생각이 들었다.
별로 해준 일은 없지만 쿼리문을 N개의 동일한 요청에 N-1개만큼 줄일 수 있는 획기적인 방법이다.

 

지금 내가 해준 방법과 Redis를 비교하면 아래와 같다.

Spring Framework 캐시:

장점:

  1. 간단한 설정: Spring Framework의 캐시는 애플리케이션의 클래스에 애너테이션을 추가하고, 캐시 매니저를 설정하기만 하면 쉽게 사용할 수 있습니다.
  2. 높은 유연성: 다양한 캐시 구현체를 지원하므로 (예: ConcurrentHashMap, Ehcache, Caffeine 등) 필요에 따라 선택할 수 있습니다.
  3. 애플리케이션과의 통합: Spring Framework와 높은 통합성을 가지고 있어서 다른 Spring 기술과 원활하게 연동됩니다.

단점:

  1. 분산 환경에서 한계: Spring Framework의 내장 캐시는 주로 단일 서버 환경에서 사용되며, 분산 환경에서의 캐시 관리에는 한계가 있습니다.
  2. 복잡한 캐시 관리: 대규모 애플리케이션에서는 복잡한 캐시 관리가 필요할 수 있으며, 이에 대한 추가적인 구현이 필요할 수 있습니다.

Redis 캐시:

장점:

  1. 높은 확장성: Redis는 클러스터링을 지원하므로 대규모 애플리케이션에서도 확장성을 제공합니다.
  2. 빠른 읽기/쓰기: Redis는 메모리 기반으로 빠른 읽기 및 쓰기 속도를 제공하며, 메모리 내 데이터 저장으로 디스크 I/O를 최소화합니다.
  3. 데이터 구조 다양성: Redis는 다양한 데이터 구조를 지원하므로 캐시 이외에도 다양한 용도로 활용할 수 있습니다.
  4. 데이터베이스의 백업 및 복원: Redis는 데이터베이스의 백업 및 복원을 지원하여 데이터 손실을 방지할 수 있습니다.

단점:

  1. 별도의 설정 및 관리가 필요: Redis를 사용하려면 별도의 서버 설정과 운영 관리가 필요하며, Redis 서버를 실행하고 관리하는 데 추가 리소스가 필요합니다.
  2. 네트워크 오버헤드: 외부 캐시 서버에 액세스하는 데 네트워크 오버헤드가 발생할 수 있으며, 이로 인해 일부 지연이 발생할 수 있습니다.

내일은 꼭 Redis를 적용시켜보자!

'프로젝트 > 미니' 카테고리의 다른 글

Eclipse, MS-SQL 연습용 프로젝트  (2) 2024.10.10
웹 미니 프로젝트 총평  (1) 2023.10.03
Cache에 Redis 적용시키기  (0) 2023.09.23
프론트엔드 개발  (0) 2023.09.22
코드 병합  (0) 2023.09.18