🎯 무엇을 개선하지?
현재 진행중인 보증금 사기 방지 프로젝트에서 Redis 를 이용해 조회 성능을 개선했었습니다.
https://comumu.tistory.com/143
Redis 로 조회 기능 개선하기
🎯 무엇을 개선하지?현재 진행중인 프로젝트에서 모든 집 게시글을 조회하는 기능 이 있습니다. 아래 화면에서 사용되고 있는 기능인데 현재는 DB 에 조회해 현재 판매중인 집 게시글을 모두
comumu.tistory.com
하지만, Cache Miss 가 되었을때 DB 에 접근해 모든 집 게시글을 조회하는 기능의 속도가 느린점이 단점이었습니다. 쿼리 튜닝을 통해 개선을 해도 한계점이 분명이 존재했습니다.
지금 당장은 문제가 없어도 만약 데이터가 10000개, 100000개 까지 쌓였을때 이에 대한 처리 방법을 개선할 필요가 있다 느꼈습니다. 그래서 찾은 방법이 바로 ElasticSearch 를 이용한 테이블 분리였습니다. ElasticSearch 를 이용해 별도의 집 관리 인덱스(테이블)을 만들고, 새로운 집 게시글이 저장될때 DB 와 ElasticSearch Index 에 저장한 뒤 모든 집 게시글을 조회할때는 인덱스 테이블에 대해 조회하고 캐싱 처리를 한다면 초기 데이터를 조회할때에 대한 성능을 향상시킬 수 있음을 알게 되었습니다.
🎯 ElasticSearch Index 만들기
가장 먼저 집 데이터를 관리할 별도의 Index 를 만들어야 합니다. Entity 의 데이터를 모두 저장하는 것이 아닌, 모든 집을 조회할때 필요한 데이터만을 선별해 만들었습니다. 현재 HomeOverViewReponse 라는 DTO 가 있었고, 해당 데이터에 있는 필드값들을 참고해 인덱스를 만들었습니다.
✅ HomeDocument
@Document(indexName = "home")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class HomeDocument {
@Id
private Long id;
@Field(name = "address")
private String address;
@Field(name = "latitude")
private double latitude;
@Field(name = "longitude")
private double longitude;
@Field(name = "main_image")
private String mainImage;
@Field(name = "rent")
private Integer rent;
@Field(name = "bond")
private Integer bond;
@Field(name = "bill")
private Integer bill;
@Field(name = "bed_room_count")
private Integer bedRoomCount;
@Field(name = "bath_room_count")
private Integer bathRoomCount;
@Field(name = "type")
private String type;
@Field(name = "user_id")
private Long userId;
@Field(name = "user_name")
private String userName;
@Setter
@Field(name = "home_status")
private HomeStatus homeStatus;
}
✅ HomeElasticRepository
public interface HomeElasticRepository extends ElasticsearchRepository<HomeDocument, Long> {
List<HomeDocument> findByHomeStatusIn(List<HomeStatus> statuses);
}
판매중인 집 게시물을 조회하기 위해, IN 조건으로 Enum 리스트 검색이 가능한 메서드를 만들었습니다.
✅ HomeElasticService
@Service
@RequiredArgsConstructor
public class HomeElasticService {
private final UserRepository userRepository;
private final HomeRepository homeRepository;
private final HomeElasticRepository homeElasticRepository;
private final HomeMapper homeMapper;
public void saveHomeToElastic(final Home home, final User user) {
HomeDocument document = homeMapper.homeToHomeDocument(home, user);
homeElasticRepository.save(document);
}
@Cacheable(value = "homeOverviewCache", key = "'allHomes'")
public HomeOverviewWrapper findAllSellHomes() {
List<HomeDocument> homeDocuments = homeElasticRepository.findByHomeStatusIn(List.of(HomeStatus.FOR_SALE, HomeStatus.DURING_SELL));
return new HomeOverviewWrapper(homeDocuments.stream()
.map(homeDocument -> homeMapper.homeDocumentToHomeOverviewResponse(homeDocument))
.collect(Collectors.toList()));
}
ElasticSearch 관련 기능을 따로 관리하기 위해 HomeElasticService 를 만들었습니다.
saveHomeToElastic 는 Home 엔티티 저장 시점에 호출됩니다.
findAllSellHomes 는 ElasticSearch 에 저장된 모든 집 게시물을 조회하고 @Cacheable 를 이용해 Redis 에 저장합니다. 해당 메서드를 통해 조회하면 DB 에 접근하지 않고 연관관계로 저장되어 있지 않은 ElasticSearch 인덱스 에서 조회하기 때문에 Cache Miss 가 발생했을때 조회하는 속도의 비약적인 향상을 기대할 수 있습니다. (아래 K6 부하 테스트를 통한 결과를 참고)
속도와 성능면에서는 좋은 결과를 도출할 수 있지만, DB 와 Elastic Search 의 인덱스를 같이 관리해야 하는 불편함이 존재하긴합니다. 쉽게 설명하자면 두개의 저장소를 사용하는 것이고 이 두개의 저장소에 대한 데이터 정합성 문제가 발생할 위험이 존재합니다.
때문에 집 게시물에 대한 CRUD 가 동작할때 이를 고려한 추가적인 작업을 해야 합니다. 개발자 입장에서 불편할 수 있어도, 결국 서비스는 사용자가 사용합니다. 때문에 개발자의 불편함을 감수한 성능 개선은 선택이 아닌 필수라 생각합니다.
🎯 K6 를 이용한 성능 테스트
✅ ElasticSearch 를 사용하기 전 성능 테스트
✅ ElasticSearch 를 사용한 후 성능 테스트
총 100개 데이터를 기준으로 1000번의 요청을 보낸 테스트 결과 입니다.
첫번째 요청(ElasticSearch 를 사용하기 전) 과 첫번째 요청(ElasticSearch 를 사용한 후)
⏱ 평균 요청 시간 (http_req_duration) | 187ms | 61.62ms |
⏱ 최대 요청 시간 | 4.48s (굉장히 높음) | 274.18ms |
응답 대기 시간 (waiting) | 181.07ms | 55.54ms |
수신 데이터량 | 33 MB | 33 MB |
송신 데이터량 | 101 kB | 101 kB |
처리 속도 (req/s) | 24.84 req/s | 27.70 req/s |
90% 응답 시간 (p90) | 154.96ms (하지만 평균은 느림) | 199.81ms |
평균 반복 소요 시간 | 1.18s | 1.06s |
VUs 수 | 10~30 | 10~30 |
ElasticSearch 를 사용한 API는 전체적으로 빠르고 안정적입니다. 평균 응답 시간과 대기 시간이 모두 낮고, 최대값도 274ms 수준으로 양호합니다.
ElasticSearch 를 사용하지 않은 API는 일부 요청에서 지연이 심합니다. 아마 캐시 저장소에 데이터가 없는 경우 발생한 지연 문제로 추정됩니다. 최대 응답 시간이 무려 4.48초로 매우 높고, 평균 응답 시간도 3배 이상 더 느립니다.
Redis 캐시만으로는 Cache Miss 시 DB 조회 속도에 한계가 있어, ElasticSearch를 도입해 별도 인덱스로 집 게시글을 관리하고 조회 성능을 개선했습니다.
ElasticSearch 인덱스는 필요한 필드만 저장하며, @Cacheable을 통해 Redis와 함께 사용하여 Cache Miss 상황에서도 빠른 응답을 제공합니다.
K6 부하 테스트 결과, 평균 요청 시간이 약 67% 단축되어 성능이 크게 향상된 것을 확인했습니다.
이처럼 ElasticSearch 와 Redis 의 조합은 데이터와 요청수가 많을수록 비약적인 성능 향상을 기대할 수 있습니다.
'Trouble Shooting && 성능 개선' 카테고리의 다른 글
[Trouble Shooting] 쿼리 로그 저장 기능 개선하기 (2) (2) | 2025.06.11 |
---|---|
[Trouble Shooting] 쿼리 로그 저장 기능 개선하기 (1) (0) | 2025.05.24 |
[Trouble Shooting] Redis Master-Replica 구조에서의 권한 문제 (0) | 2025.03.10 |
[Trouble Shooting] 비관적 락을 이용한 금융기능 동시성 제어 (0) | 2025.02.02 |
[Trouble Shooting] 서비스 운영에 필요한 로그 관리 방법 with CloudWatch (0) | 2025.01.28 |