Users 테이블에 100만 개의 데이터를 넣고 조회 속도 개선을 위해 2가지를 시도해 보았습니다.
이를 위해 간단한 닉네임을 조회하는 메서드를 만들었습니다.
public UserSearchResponse searchNickname(final UserSearchRequest userSearchRequest) {
final User user = userRepository.findByNickname(userSearchRequest.nickname())
.orElseThrow(() -> new InvalidRequestException("닉네임이 유효하지 않습니다."));
return UserSearchResponse.from(user.getNickname());
}
실행시간은 다음과 같이 302ms, 296ms 나왔습니다.
이제 이 실행 시간을 줄여보겠습니다.
1. 인덱스(Index)
인덱스란 책의 색인처럼 데이터베이스에서 특정 데이터를 빠르게 찾기 위한 데이터 구조입니다. 테이블의 특정 컬럼을 기준으로 데이터의 위치 정보를 저장하여 검색 속도를 높이는 역할을 합니다. 인덱스를 활용하면 전체 데이터를 하나하나 검색하는 Full Table Scan을 피하고, 필요한 데이터에 빠르게 접근할 수 있습니다.
@Table(name = "users",
indexes = @Index(name = "idx_nickname", columnList = "nickname"))
다음과 같이 원하는 컬럼에 인덱스를 걸 수 있습니다.
인덱스는 어느 컬럼에 적용하면 좋을까?
중복도가 적은 컬럼에 적용하는 게 좋습니다. (카디널리티가 높다)
기본적으로 PK에는 인덱스가 적용되어 있고, 또한 Unique 컬럼에도 적용되어 있습니다.
저의 email 컬럼은 Unique 컬럼이라 총 3개의 컬럼에 인덱스가 걸려있는 걸 확인할 수 있습니다.
인덱스를 적용했을 때
기존에 302ms, 296ms → 28ms, 21ms로 유의미한 속도 향상이 있었습니다.
장점
- 빠른 검색 속도: 데이터베이스에서 대량의 데이터를 다룰 때 검색 시간이 크게 줄어듭니다. 특히 자주 조회되는 컬럼에 인덱스를 적용하면 성능을 크게 향상할 수 있습니다.
- Full Table Scan 방지: 테이블을 처음부터 끝까지 검색하는 방식(FULL TABLE SCAN)을 피할 수 있습니다.
단점
- 쓰기 성능 저하: 인덱스는 Insert, Update, Delete 작업 시 성능을 저하시킬 수 있습니다. 데이터가 추가되거나 변경될 때마다 테이블 구조를 다시 재설정해야 하기 때문입니다.
- 디스크 공간 차지: 인덱스는 별도의 공간을 차지합니다. 특히 큰 테이블에 대해 여러 개의 인덱스를 만들 경우, 저장 공간의 사용량이 증가할 수 있습니다.
2. Redis
Redis는 인메모리 데이터 저장소로, 매우 빠른 읽기/쓰기 성능을 제공합니다. 주로 데이터베이스와 함께 캐시로 사용되어, 반복적인 데이터 요청에 대해 성능을 극대화할 수 있습니다. 데이터를 Redis에 미리 저장해 두고, 자주 조회되는 데이터를 데이터베이스를 거치지 않고 Redis에서 바로 읽어오는 방식입니다.
Redis에 캐시 된 데이터는 메모리에서 빠르게 검색할 수 있기 때문에, 디스크 기반 데이터베이스에서 데이터를 찾는 것보다 훨씬 빠른 속도로 처리할 수 있습니다.
이를 위해 레디스를 사용하는 간단한 메서드를 만들어보았습니다.
public UserSearchResponse searchNickname(final UserSearchRequest userSearchRequest) {
final String nickname = userSearchRequest.nickname();
final String redisKey = REDIS_KEY_PREFIX + nickname;
final UserSearchResponse cachedResponse = (UserSearchResponse) redisTemplate.opsForValue().get(redisKey);
if (cachedResponse != null) {
return cachedResponse;
}
final User user = userRepository.findByNickname(nickname)
.orElseThrow(() -> new InvalidRequestException("닉네임이 유효하지 않습니다."));
final UserSearchResponse response = UserSearchResponse.from(user.getNickname());
redisTemplate.opsForValue().set(redisKey, response, 10, TimeUnit.MINUTES);
return response;
}
처음에는 568ms가 걸리는 걸 확인할 수 있습니다.
하지만 두 번째 에는 66ms로 엄청 작은 시간으로 줄어들었습니다.
이유가 무엇일까?
기본 흐름
레디스 흐름
처음 Redis에 데이터가 없을 때는 DB에서 데이터를 읽는 데 시간이 걸리지만, 처음에 Redis에 저장을 했으므로 두 번째에서는 Redis에서 바로 데이터를 가져오기 때문에 훨씬 빠른 속도가 나옵니다.
User65011가 레디스에 저장되어 있는 걸 확인할 수 있습니다.
장점
- 빠른 데이터 접근: Redis는 메모리 기반으로 동작하므로, 매우 빠른 속도로 데이터를 읽을 수 있습니다.
- 데이터베이스 부하 감소: 만약 조회하길 원했던 데이터가 Redis에 있을 때 DB를 거치지 않아도 되므로 DB부하가 감소됩니다.
단점
- 초기 캐시 미스: 캐시 된 데이터가 없을 때는 Redis에서 데이터를 가져오는 데 시간이 걸립니다.
- 메모리 제약: Redis는 메모리 기반으로 작동하기 때문에, 저장할 수 있는 데이터의 양에 제한이 있습니다.
- 캐시 일관성 문제: 데이터베이스와 Redis 캐시 간의 데이터 정합성에 문제가 있을 수도 있습니다.
'WEB' 카테고리의 다른 글
@Async를 통한 이메일 비동기 처리 (0) | 2024.12.29 |
---|---|
Spring Security를 사용해 JWT 인증하기 (1) | 2024.11.18 |
[JPA] N+1 문제와 프록시 강제 초기화 해결 (0) | 2024.10.23 |
[WebClient] WebClient를 사용한 날씨 API 리팩토링 (0) | 2024.10.15 |
[JPA] JPA Update 실패 해결기 (0) | 2024.10.10 |
소프트 딜리트란? (1) | 2024.09.27 |
Spring Data JPA로 된 코드를 JDBC로 다시 짜보기 (1) | 2024.09.25 |
MDC를 이용해 세부 로그 확인하기 (0) | 2024.09.23 |