개발자로 후회없는 삶 살기
[최적화] Cache 도입을 위한 데이터 접근 방식 문제점 분석하기 본문
🚨 서론 (문제 상황)
필자가 운영하고 있는 AI 검색 서비스는 4대의 분산 추론 서버의 응답을 저장하고, 클라이언트에게 전시하는 동작이 가장 중요한 기능 중 하나이다.
추론 요청에 대한 응답으로는 단순 문자열 1개나 이미지가 아닌, 100개 이상의 검색 결과로 1개의 검색 결과에 5개의 필드를 가지고 있다.
이처럼 한 번의 추론에서 50KB 용량의 다수의 문자열 데이터를 응답 받기에 어떻게 클라이언트에게 전달하는 것이 효과적일지 고민이 생겼다.
또한, 클라이언트에 추론 결과를 제공하기 위해서 추론 결과가 저장되는 공간을 반복적으로 체크하도록 설계가 되어있었기 때문에 접근 시간, 조회 속도를 고려하여 저장 공간을 선택해야 했다. 필자가 최종 선택한 저장 공간은 시간 지역성을 고려한 Redis Cache이며, 선택한 이유를 설명한다.
본론
// 한 땀 한 땀 저장
public void saveResult(final Long messageId, final InferencesResponse inferencesResponse) {
final Message message = messageRepository.findById(messageId)
.orElseThrow(() -> new NoSuchMessageException(messageId));
final Clothes clothes = clothesRepository.findByMessage(message)
.orElseThrow(() -> new NoSuchClothesException(messageId));
final ClothesResult clothesResult = resultRepository.findByMessage(message)
.orElseThrow(() -> new NoSuchMessageException(messageId));
inferencesResponse.getResult().stream()
.forEach(findResponse -> {
final Search search = new Search(
findResponse.getThumbnailUrl(),
findResponse.getWebSearchUrl(),
findResponse.getHostPageUrl(),
findResponse.getName(),
findResponse.getSite(),
clothesResult
);
searchRepository.save(search);
});
}
기존엔 추론 결과를 JPA를 사용하여 RDB에 저장했다. 100개 이상의 데이터를 jpa save()로 한 줄 한 줄 저장하는 코드이다.
1) 100개 이상의 결과를 100개 이상의 RDB ROW에 각각 Insert 문으로 저장하는 시간 발생 -> 레이턴시 증가
2) 반복 재귀 호출 시, [프론트 -> 서버 -> DB 서버] 접근으로 인한 네트워크 접근 시간 및 SELECT DB IO 발생
3) 추론 결과 응답 시, 다수의 검색 데이터 네트워크 IO 발생
RDB에 저장했을 때 문제점은 위와 같으며, 데이트 응답 속도가 너무 느려서 개선이 필요한 상황이었다.
🚨 RDB를 사용했을 때 문제점
1. 다수의 Insert 문 발생
한 번의 검색에서 100개 이상의 Insert 문이 발생하는데, 이는 어플리케이션 서버와 DB 서버 간의 네트워크 IO와 DB IO가 검색 데이터 개수만큼 발생하고, Insert가 끝나야 클라이언트에게 결과를 응답할 수 있는 설계 특성상 레이턴시가 길어지는 문제가 발생한다.
2. 다수의 검색 데이터 접근 시간 및 IO 시간 발생
비동기 재귀 호출로 추론이 완료 되었는지 검사를 하게 되는데, 검사 공간이 DB 서버라서 클라이언트에서 어플리케이션 서버로 가고, 또 DB 서버로 접근하는 시간이 발생한다.
if(searchRepository.existsByClothes(clothesResult)) {
final List<SearchResponseDto> searchResponseDtos = searchRepository.findAllByClothes(clothesResult)
.stream()
.map(SearchResponseDto::from)
.collect(Collectors.toList());
return SearchsResponseDto.from(searchResponseDtos);
}
커밋 시점에 DB에 반영되는 것을 고려하여 DB에 데이터가 생성됐을 때 클라이언트에게 추론 결과를 전달하는 코드를 작성했다. 재귀 호출을 하는 동안에도 select 쿼리가 발생하여 DB IO가 증가하고, 응답 시간도 대부분 10 ms 이상이다.
또한, 검색 데이터를 불러오는 시간도 143ms로 비교적 길며, RDB에 모든 행을 저장하는 시간이 오래 걸려서 평균 10번의 재귀 호출이 발생한다. 이제부터 다수의 추론 결과를 저장하고 클라이언트에게 응답할 때 필자가 해결한 방법을 알아보자.
✅ Redis를 사용했을 때 유의미한 개선점
1. RDB Insert문 제거
반복 재귀 호출 동안, DB에 접근하지 않고, 서버의 메모리 DB에 접근하여 네트워크 IO가 발생하지 않고, Redis Key-Value만 검사하여 DB 쿼리도 발생하지 않는다.
2. 다수의 검색 데이터 접근 시간 단축
한 번에 검색 데이터를 Redis에 저장하여, 저장하는 시간이 단축되어 평균 6번의 재귀 호출이 발생한다. (기존 대비 4번 감소) 각 재귀 호출마다 서버 메모리의 Redis만 접근하면 되므로 10초 내외의 검사 시간이 소요된다.
가장 유의미한 개선 점은, 전체 검색 데이터를 불러오는 시간이 143ms에서 19ms로 기존 대비 86.7% 감소했다.
결론
Redis를 도입해야 하는 상황에는 반드시 시간과 공간 지역성을 따져봐야 한다. 필자의 경우 시간과 공간 지역성이 모두 필요했기 때문에 유의미한 개선이 있다. CS 지식을 기반한 최적화 경험이라고 생각한다.
'[백엔드] > [spring+JPA | 이슈해결]' 카테고리의 다른 글
[최적화] Redis Stream 내부 ObjectHashMapper를 이용하여 HASH 역직렬화 자동화하기 (2) | 2024.11.24 |
---|---|
[최적화] Redis Stream에 적절한 RedisSerializer를 사용하자 (2) | 2024.11.23 |
[최적화] Spring 환경 AI 서비스 실시간 스트림 파이프라인 구축 (with Redis Stream) (0) | 2024.11.11 |
자바 PART.무한의 값 처리 BigDecimal (0) | 2023.11.27 |
spring PART.postman으로 login 테스트 할 때 받아온 토큰을 요청 헤더에 자동으로 넣는 방법 (0) | 2023.08.18 |