성능 최적화 요약
Chat Project
Chat Project를 진행하면서 성능 개선을 위해 여러 가지 최적화 작업을 진행했다.
이를 통해 로딩 시간, 조회 속도 및 응답 시간을 크게 개선했다.
아래는 각 주요 최적화 작업과 성능 테스트 결과의 요약이다.
더 자세한 내용은 관련 링크에서 확인할 수 있다.
1. 채팅 기록 파일 조회 속도 개선
2. 채팅 기록 파일 조회 횟수 감소
3. Broker 적용 : STOMP 프로토콜을 지원하는 RabbitMQ Broker 적용
4. db 조회 횟수 감소 : 채팅을 하는데 필요한 정보들을 Redis Cache를 활용해 db 조회 횟수 감소
1. 채팅 기록 파일 조회 속도 개선
처음에는 줄바꿈을 포함해 사람이 읽기 쉬운 형태로 채팅 기록 파일을 저장했다.
하지만 데이터가 쌓이면서 조회 속도가 점차 느려졌고 사용자가 불편할 정도로 로딩 시간이 길어졌다.
이를 개선하기 위해 채팅 기록을 한 줄로 저장하는 방식으로 변경했고 이를 통해 조회 속도가 크게 개선되었다.
관련 글 : Jmh
2. 채팅 기록 파일 조회 횟수 감소
초기에는 채팅 버튼을 반복해서 열고 닫을 때마다 마지막 메시지를 매번 조회하는 비효율적인 방식이었다.
이를 개선하기 위해 가장 최근 메시지를 HashMap에 저장해 두고
채팅창을 열 때 db 조회 대신 Map에 저장된 값을 참조하도록 변경했다.
마지막 글을 조회할 때 hashMap에 해당 기록을 저장하고 채팅을 할 때마다 해당 내용을 update했다.
그 결과, Map 사용 시 조회 속도가 현저히 개선되었다.
- Map 미사용: 0.283초 (283,892,100 나노초)
- Map 사용: 0.017초 (17,185,200 나노초)
3. Broker 적용 : STOMP 프로토콜을 지원하는 RabbitMQ Broker 적용
Stomp 프로토콜을 사용할 때 기본적으로 In-Memory Message Broker를 사용했는데
In-Memory Message Broker는 용량 제한, 메시지 유실 가능성, 모니터링 어려움 등의 문제점이 있었다.
이를 개선하고자 외부 메시지 브로커인 RabbitMQ를 도입했다.
관련 글 : RabbitMQ
4. db 조회 횟수 감소
Redis Cache를 활용해 자주 참조되는 데이터를 메모리에 저장하여 DB 조회 횟수를 줄였다.
전체 값을 캐싱할 때는 @Cacheable
과 같은 어노테이션을 사용했으며
일부 값만 필요할 때는 RedisTemplate
을 활용했다.
이를 통해 최소한의 DB 조회만 하도록 코드를 작성했다.
관련 글 : Redis Cache
추가로 채팅의 마지막 메시지(LastMessage)는 항상 최신 데이터를 가지고 있어야 했다.
초기에는 @Cacheable
을 사용하려 했으나 캐시 갱신을 위해 @CachePut
을 적용했다.
하지만 메서드를 항상 실행되는 @CachePut
가 실제로 캐시의 효율성을 높이는지 고민이 되었고
두 가지 캐시 접근 방식의 성능을 테스트했다.
테스트는 Controller에서 Service 로직 실행 후 응답 시간을 측정하는 방식으로 진행했다.
1. @CachePut
사용
2. RedisTemplate
을 사용해 LastMessage만 제외하고 나머지 값만 캐시에 저장하는 방식
@CachePut
적용 코드
1
2
3
4
@CachePut(key = "#nickname", value = "createRoom", unless = "#result == null", cacheManager = "cacheManager")
public ChatRoomDto createRoom(String nickname) {
// 코드 생략
}
RedisTemplate
적용 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 일부 코드는 생략했다.
public ChatRoomDto createRoom(String nickname) {
if (!chatRepository.existsByUserId(user.getId())) {
redisService.addCreateRoom("createRoom::"+nickname, chatRoom);
return chatRoom;
} else {
ChatRoomDto cachedChatRoomDto = redisService.getCreateRoom("createRoom::" + nickname);
if (cachedChatRoomDto != null) {
cachedChatRoomDto.setLastMessage(getLastMessage(cachedChatRoomDto.getRoomId()));
return cachedChatRoomDto;
} else {
log.info("[createRoom] cache 적용 x");
redisService.addCreateRoom("createRoom::"+nickname, chatRoom);
return ChatRoomDto.of(user, lastLine);
}
}
}
성능 테스트 결과
실행 시간은 나노초 단위로 측정되었으며 아래는 초 단위로 변환한 결과다.
Map 활용 x +
@CachePut
: 0.089초 (89473300 나노초)
Map 활용 o +
@CachePut
: 0.018초 (17525700 나노초)
Map 활용 x +
RedisTemplate
: 0.051초 (51041600 나노초)
Map 활용 o +
RedisTemplate
: 0.009초 (8879100 나노초)
테스트 결과 RedisTemplate
을 활용한 방식이 @CachePut
보다 응답 속도가 더 빨랐다.
그 이유에 대해서 추측해보자면
@CachePut
은 메서드가 실행될 때마다 전체 반환값을 캐시에 갱신하면서 변하지 않는 고정 데이터까지 불필요하게 업데이트를 하게된다.
반면 RedisTemplate
을 활용한 로직은 고정된 데이터만 캐시에 저장하고 변동되는 데이터(LastMessage)만 조회한다.
이 방식은 변하지 않는 데이터를 불필요하게 갱신하지 않아 더 효율적이며
Map과 함께 사용해 자주 접근하는 데이터를 메모리에 유지하고
변동되는 데이터만 별도로 갱신할 수 있어 성능을 개선할 수 있었던 것 같다.