-
Notifications
You must be signed in to change notification settings - Fork 0
채팅방 불러오기 API DB 쿼리 수정을 통한 응답 시간 개선
-
부스트캠프 프로젝트 도중 한 사람의 채팅방 목록을 가져오는 API 를 구현했다. 이 API 는 우리가 흔히 사용하는 카카오톡을 들어가보면 나오는 채팅방 목록에서 보여지는 채팅 상대 이름, 채팅 상대의 프로필 사진, 마지막 채팅 , 안 읽은 채팅이 있는지에 대한 알림 등을 가져오는 API 이다.
-
성능 테스트를 위해 한 사람에 대해 채팅방 30개를 만들고 각 방에 대략 15000개의 채팅을 넣었다. 총 45만개의 채팅 데이터를 DB 에 INSERT 했다.
-
성능 테스트를 해보기 전 까지는 그냥 괜찮은 응답속도라 생각했었다. 50ms 내외로 계속 응답이 왔었고 이정도면 나쁘지는 않다라고 판단했었는데 이후에 채팅 데이터를 넣고 다시 테스트를 해보니까 처참한 결과가 나왔다.
async findRoomList(userId: string){
let now = new Set();
const rooms = await this.chatRoomRepository
.createQueryBuilder('chat_room')
.select([
'chat_room.user',
'chat_room.writer',
'chat_room.id',
'chat_room.post_id',
'chat.message',
'chat.create_date',
])
.where('chat_room.user = :userId', {
userId: userId,
})
.orWhere('chat_room.writer = :userId', {
userId: userId,
})
.leftJoin('chat', 'chat', 'chat_room.id = chat.chat_room')
.orderBy('chat.id', 'DESC')
.addSelect(['user.w.user_hash', 'user.w.profile_img', 'user.w.nickname'])
.leftJoin('user', 'user.w', 'user.w.user_hash = chat_room.writer')
.addSelect(['user.u.user_hash', 'user.u.profile_img', 'user.u.nickname'])
.leftJoin('user', 'user.u', 'user.u.user_hash = chat_room.user')
.addSelect(['post.thumbnail', 'post.title'])
.leftJoin('post', 'post', 'post.id = chat_room.post_id')
.getRawMany();
const result = rooms
.reduce((acc, cur) => {
acc.push({
room_id: cur.chat_room_id,
post_id: cur.chat_room_post_id,
post_title: cur.post_title,
post_thumbnail: cur.post_thumbnail,
user: cur['user.w_user_hash'],
user_profile_img: cur['user.w_profile_img'],
user_nickname: cur['user.w_nickname'],
writer: cur['user.u_user_hash'],
writer_profile_img: cur['user.u_profile_img'],
writer_nickname: cur['user.u_nickname'],
last_chat: cur.chat_message,
last_chat_date: cur.chat_create_date,
});
return acc;
}, [])
.sort((a, b) => {
return b.last_chat_date - a.last_chat_date;
})
.reduce((acc, cur) => {
if (!now.has(cur.room_id)) {
acc.push(cur);
now.add(cur.room_id);
}
return acc;
}, []);
return result;
}
-
현재 10회 반복한 결과인데 더 여러번 해볼 가치조차 느끼지 못했고 바로 API 에 대한 개선이 필요하다고 생각했다. 데이터 대략 45만개에 API 응답 시간이 10초라니,,, 이건 과장 좀 보태서 내가 처리하는게 더 빠르겠다는 생각이 들었다.
-
그래서 현재 내 API 에 어떤 문제점이 있는지, 성능 저하를 하는 요소는 무엇이 있을지 찾아보았고 여러가지 개선 해야할 점을 작성할 수 있었다.
-
일단 2번 작업에서 채팅방들을 최신순으로 업데이트 하는 작업은 서버나 DB 둘 중 하나에서는 무조건 처리해야 한다.
-
가장 큰 문제는 1번이었다. 현재 DB table 은 chat 과 chat_room 이 존재했고 chat_room 의 id 와 chat 의 chat_room 칼럼이 외래키로 연결되어 있었다. 이 chat_room 을 가져오는 과정에서 해당 chat_room 에 대한 chat 을 모두 Join 해서 가져오는 문제가 있었다. 즉 채팅방에 채팅이 10개가 있으면 해당 chat을 모두 Join 해서 가져온다.
-
3번도 큰 영향은 아니지만 DB에서 처리해서 가져올 수 있지 않을까? 라고 생각이 들었다. profile_img 의 값이 null 인 경우 기본 이미지 url 로 가져오는 쿼리가 있을 것 같았다.
-
먼저 채팅목록을 모두 가져오는 방법 말고 각 채팅방의 마지막 채팅만 가져오는 방식을 찾아보았다. 그래서 이에 대해 찾아보다가 MySQL 에 GROUP BY 쿼리를 알게 되었다. GROUP BY 는 데이터를 정해진 Column 으로 묶어서 해당 그룹에 대한 데이터를 추출하는데 사용된다.
-
이 GROUP BY 를 알았을 때, "아 그러면 GROUP BY 를 해서 각 그룹마다 가장 최근에 작성된 채팅 데이터를 가져오면 되겠구나!" 라고 생각하고 바로 쿼리를 작성했다. 이때 작성한 SQL 이 다음과 같았다.
SELECT MAX(id) , message, sender, chat_room FROM chat
GROUP BY chat_room
-
그런데 이 쿼리는 내가 원하는 데이터를 가져오지 않았다. 현재 우리의 DB TABLE 에서는 id 가 클수록 최근에 작성된 채팅이기 때문에 MAX(id) 로 가져오면 당연히 해당 채팅 데이터가 다 불러올거라 생각했다. 생각과 달리 이 쿼리는 id 는 Max 인 값을 가져오는데 나머지 값이 id 가 max 인 row 의 Column 들을 가져오는 것이 아닌 다른 값들을 가져왔다.
-
그래서 그러면 이 id 가 MAX(id) 인 row 를 가져오면 되겠다라고 판단되었다. 여기서 서브쿼리를 사용하면 된다고 해서 처음으로 서브쿼리를 사용해보았다. 여기까지 각 채팅방에서 가장 최신 채팅 데이터를 가져오는 부분이다.
SELECT * FROM chat
WHERE id IN
(SELECT MAX(id) FROM chat
GROUP BY chat_room)
- 아까 개선점에서 1번에 대한 개선이 크게 이뤄졌다. 모든 채팅목록을 가져와서 최대값을 서버에서 계산하는것이 아닌 이미 DB 에서 가져올 때 부터 채팅방과 각 채팅방에 대한 마지막 채팅정보를 가져오게 되었다.
IFNULL(profile_img, url)
- 그리고 나머지 table 을 JOIN 한 이후, IFNULL 구문 을 이용하여 프로필 이미지가 null 값인 것들을 서버에서 처리하는 것이 아니라 애초에 DB에서부터 가져올 때, url 값으로 가져올 수 있게 되었다. 이에 더해서 아예 서버에서 해당 쿼리에 대한 추가적인 처리를 없애기 위해 ORDER BY 까지 SQL 쿼리로 처리했다.
- DB 에서 클라이언트에 필요한 모든 처리를 마치고 서버에서 추가적인 작업을 없앴다.
async findRoomList(userId: string) {
const subquery = this.chatRepository
.createQueryBuilder('chat')
.select('chat.id', 'id')
.addSelect('chat.chat_room', 'chat_room')
.addSelect('chat.message', 'message')
.addSelect('chat.create_date', 'create_date')
.addSelect('chat.is_read', 'is_read')
.addSelect('chat.sender', 'sender')
.where(
'chat.id IN (SELECT MAX(chat.id) FROM chat GROUP BY chat.chat_room)',
);
const rooms = await this.chatRoomRepository
.createQueryBuilder('chat_room')
.innerJoin(
'(' + subquery.getQuery() + ')',
'chat_info',
'chat_room.id = chat_info.chat_room',
)
.innerJoin(
'chat_room.writerUser',
'writer',
'chat_room.writerUser = writer.user_hash',
)
.innerJoin(
'chat_room.userUser',
'user',
'chat_room.userUser = user.user_hash',
)
.leftJoin('chat_room.post', 'post', 'chat_room.post = post.id')
.select([
'chat_room.id as room_id',
'chat_room.writer as writer',
'writer.nickname as writer_nickname',
'IFNULL(writer.profile_img , "https://kr.object.ncloudstorage.com/village/732e917b-28ee-4096-b4dc-4a9229d6d928") as writer_profile_img',
'chat_room.user as user',
'user.nickname as user_nickname',
'IFNULL(user.profile_img , "https://kr.object.ncloudstorage.com/village/732e917b-28ee-4096-b4dc-4a9229d6d928") as user_profile_img',
'chat_room.post_id as post_id',
'post.title as post_title',
'post.thumbnail as post_thumbnail',
'chat_info.create_date as last_chat_date',
'chat_info.message as last_chat',
'chat_info.is_read as all_read',
'chat_info.sender as sender',
])
.where('chat_room.writer = :userId', { userId: userId })
.orWhere('chat_room.user = :userId', { userId: userId })
.orderBy('chat_info.create_date', 'DESC')
.getRawMany();
return rooms;
}
-
985ms 는 분명 아직 느리긴 하지만 그래도 10배 가까이 API 의 응답 속도가 빨라졌다.
-
일단 할 수 있는 처리는 모두 한 듯 하다. ORDER BY , GROUP BY 에 index 를 걸어놓는 작업까지 진행했고 쿼리는 많이 개선했다.
-
여기서 더 개선하려면 서브쿼리를 캐싱하여 각 채팅방에 대한 MAX(id) 를 매번 구하는 것이 아니라 캐시한 데이터를 가져오면 더 빠르게 응답할 수 있지 않을까? 라는 생각으로 이에 대해 찾아봤었는데 MySQL 이 5.7.x 부터 쿼리 캐싱을 지원하지 않는다는 것을 알았다.
-
이 방법 말고 더 빠른 방법이 있을 듯 한데 아직 SQL 에 대해 많이 알지 못해서 그런가 해당 방법을 적용하기가 어렵다... 적용시키려고 하면 어딘가에서 구문 오류가 나거나 invalid 하다.
-
분명히 채팅목록 중 최대인 것들을 서브쿼리 처리 없이 chat 과 chat_room 테이블 join 으로 처리할 수 있을 듯 한데... 더 공부하면서 개선해봐야겠다.
-
아니면 마지막 채팅 내역을 chat_room table 에 같이 저장하면서, 채팅을 저장할 때, 채팅 저장과 chat_room table 의 업데이트를 한 트랜잭션 내에서 처리하면 효율적으로 할 수 있지 않을까 생각이 든다. 이해 대해 실험해봐야겠다.
- 위에 작성한 마지막 채팅 내역의 id 를 chat_room table 에 같이 저장하여 서브쿼리를 사용하지 않고 그냥 바로 chat 과 join 하여 최종적으로 52ms 까지 쿼리를 개선했다!