Rust에서의 Concurrent Database Connection과 쿼리 실행
목차
서론 #
이 글은 프로젝트 댓글이에 관한 제 기사를 이미 읽으셨다면 더욱 풍부한 정보를 제공할 수 있습니다. 이 글은 댓글이 프로젝트에서 파생된 내용입니다.
이 글에서는 데이터베이스 connection을 초기화하고 동시에 쿼리를 실행하는 과정에서 발견한 내용을 공유합니다.
예제 코드 #
제공된 Rust 코드는 웹 애플리케이션에서 주어진 방에 대한 댓글을 검색하는 두 개의 비동기 함수, find_comment_by_room
과 find_best_comment_by_room
을 정의합니다. 두 개의 함수는 모두 특정 채팅방에서의 댓글을 반환받는 목적을 가진 함수입니다.
/// [GET /room/comment?offset=0&limit=10?is_user=true] 채팅방에서 모든 댓글을 찾습니다
async fn find_comment_by_room(
req: HttpRequest,
body: web::Json<RoomUrlDto>,
query: web::Query<RoomQueryString>,
state: web::Data<AppState>,
) -> impl Responder {
let url = Comment::remove_query_string(&body.url);
let comments = match postgres::comment::find_comment_by_room(
&state.postgres,
&url,
query.get_offset(),
query.get_limit(),
)
.await
{
Ok(comments) => comments,
Err(e) => {
return HttpResponse::InternalServerError().json(JsonMessage {
msg: format!("Error: {:?}", e),
})
}
};
if query.get_is_user() == false {
return HttpResponse::Ok().json(comments);
}
let user_uuid = match validate_request(req, &state.postgres).await {
Ok(uuid) => uuid,
Err(e) => return e,
};
let mut result: Vec<CommentResponse> = vec![];
// TODO can run asynchronously
for comment in comments {
let comment_like = match postgres::comment::find_one_comment_like(
&state.postgres,
&user_uuid,
&comment.uuid,
)
.await
{
Ok(comment_like) => {
if comment_like.is_none() {
0
} else {
comment_like.unwrap().like
}
}
Err(e) => return HttpResponse::InternalServerError().json(format!("Error: {:?}", e)),
};
result.push(CommentResponse::new(comment, comment_like));
}
HttpResponse::Ok().json(result)
}
/// [GET /room/comment/best?offset=0&limit=10&is_user=true] 채팅방의 베스트 댓글 찾습니다
async fn find_best_comment_by_room(
req: HttpRequest,
body: web::Json<RoomUrlDto>,
query: web::Query<RoomQueryString>,
state: web::Data<AppState>,
) -> impl Responder {
let url = Comment::remove_query_string(&body.url);
let comments = match postgres::comment::find_best_comment_by_room(
&state.postgres,
&url,
query.get_offset(),
query.get_limit(),
)
.await
{
Ok(comments) => comments,
Err(e) => {
return HttpResponse::InternalServerError().json(JsonMessage {
msg: format!("Error: {:?}", e),
})
}
};
if query.get_is_user() == false {
return HttpResponse::Ok().json(comments);
}
let user_uuid = match validate_request(req, &state.postgres).await {
Ok(uuid) => uuid,
Err(e) => return e,
};
let mut futures = vec![];
let mut result: Vec<CommentResponse> = vec![];
for comment in comments.iter() {
futures.push(postgres::comment::find_one_comment_like(
&state.postgres,
&user_uuid,
&comment.uuid,
));
}
let comment_like_list: Vec<i32> = futures::future::join_all(futures)
.await
.into_iter()
.map(|res| match res {
Ok(comment_like) => {
if comment_like.is_none() {
0
} else {
comment_like.unwrap().like
}
}
Err(_) => 0,
})
.collect();
let mut i = 0;
for comment in comments {
result.push(CommentResponse::new(comment, comment_like_list[i]));
i += 1;
}
HttpResponse::Ok().json(result)
}
find_comment_by_room
: 이 함수는 HTTP 요청, RoomUrlDto가 포함된 JSON 본문, RoomQueryString이 포함된 쿼리 및 애플리케이션 상태를 입력 받습니다. 이 함수는 주어진 채팅방과 관련된 모든 댓글을 데이터베이스에서 찾습니다. 그런 다음 요청이 사용자로부터 온 것인지 확인합니다. 그렇지 않은 경우 댓글을 JSON 응답으로 반환합니다. 사용자의 요청인 경우 각 댓글에 대해 사용자의 comment_like를 검색하고 CommentResponse 객체를 구성한 후 JSON 응답으로 반환합니다.find_best_comment_by_room
: 이 함수는find_comment_by_room
함수와 유사한 구조를 가지고 있지만 구현에서 몇 가지 차이가 있습니다. 특정 기준(예: 가장 많은 좋아요)에 따라 주어진 방의 최고의 댓글을 검색합니다. 요청이 사용자가 아닌 경우 댓글을 JSON 응답으로 반환합니다. 사용자의 요청인 경우 Rust의futures
라이브러리를 사용하여 모든 댓글에 대해 동시에 사용자의 comment_likes를 가져옴으로써 성능을 향상시킵니다. 그런 다음 CommentResponse 객체를 구성하여 JSON 응답으로 반환합니다.
find_best_comment_by_room
함수가 futures
라이브러리를 활용하는 방식에 대한 보다 더 자세한 설명입니다:
- 먼저 futures라는 빈 vector를 초기화하여 각 comment_like 쿼리를 저장합니다.
- 각 댓글을 반복하면서 각 comment_like 쿼리에 대해 새 future를 생성하고 futures vector에 각 future를 푸시합니다.
- 모든 comment_like 쿼리가 futures vector에 추가되면 join_all 함수를 호출합니다. 이 함수는 vector에 있는 모든 쿼리를 동시에 실행합니다.
- 동시 실행이 완료되면 결과를 처리하여 각 댓글에 대한 CommentResponse 객체를 구성한 후 JSON 응답으로 반환합니다.
futures
라이브러리를 사용함으로써 find_best_comment_by_room
은 find_comment_by_room
에 비해 상당히 향상된 성능을 달성할 수 있습니다. 처리해야 할 댓글이 많은 경우에 특히 그렇습니다. 이 동시성 기반 접근 방식은 데이터베이스 쿼리 실행 완료를 기다리는 전체 시간을 줄여 응답 시간을 빠르게 하고 웹 애플리케이션의 효율성을 높입니다.
초기 Postgres connection pool 생성 시간 #
이 섹션은 이 프로젝트를 진행하면서 가장 가치 있는 경험이었습니다. 여태껏, 동시성에 대한 고민은 시간을 절약하고 성능을 향상시킬 수 있다는 점을 언급했습니다만…
놀랍게도 초기 쿼리 실행의 경우, concurrent한 쿼리 실행은 순차 실행에 비해 훨씬 더 많은 시간이 걸릴 수 있습니다.
함수 이름 | 초기 실행 시간 | 초기 실행 후 |
---|---|---|
find_comment_by_room | 620ms | 314ms |
find_best_comment_by_room | 1313ms | 127ms |
이 결과는 제 예상과는 달랐고, 굉장희 의아한 수치였는데요. 저는 concurrent한 쿼리가 많은 시간을 절약할 수 있다고 생각했습니다. 하지만, 모든 상황에서 그런 것은 아니었으며, 쿼리를 데이터베이스에 처음 실행할 때에는, 데이터베이스에 initial connection을 생성하는 데 걸리는 시간 때문임을 깨달았습니다. find_best_comment_by_room
은 동시에 여러 쿼리를 실행하기 때문에, 동시에 여러 데이터베이스 connection을 생성해야 합니다. 이는 find_comment_by_room
의 경우처럼 단일 connection을 생성하는 것보다 더 오래 걸릴 수 있습니다.
connection pool이 준비된 이후에는, 동시 실행은 훨씬 더 효율적인 응답 시간을 가졌습니다. 초기 실행 후 find_comment_by_room
은 314ms만에 실행되었고, find_best_comment_by_room
은 단 127ms만에 실행되었습니다. 이는 병렬적인 실행이 데이터베이스 쿼리 성능을 최적화하는 효과적인 방법일 수 있음을 보여주지만, 그러나 초기 데이터베이스 connection 생성을 염두에 두는 것 또한 중요합니다.
위 코드는 POSTGRES_MAX_CONNECTION
수가 10
으로 설정되었을 때의 것입니다. POSTGRES_MAX_CONNECTION
을 1
로 설정하면 그 성능은 다음과 같습니다.
함수 이름 | 초기 실행 시간 | 초기 실행 후 |
---|---|---|
find_comment_by_room | 575ms | 560ms |
find_best_comment_by_room | 666ms | 637ms |
이는 이전 결과와 비교할 때 한 번에 여러 연결을 생성해야 하는 경우와 그다지 차이가 나지 않습니다.