메인 콘텐츠로 이동하기
  1. Posts/

Rust에서의 Concurrent Database Connection과 쿼리 실행

·770 자

서론 #

이 글은 프로젝트 댓글이에 관한 제 기사를 이미 읽으셨다면 더욱 풍부한 정보를 제공할 수 있습니다. 이 글은 댓글이 프로젝트에서 파생된 내용입니다.

이 글에서는 데이터베이스 connection을 초기화하고 동시에 쿼리를 실행하는 과정에서 발견한 내용을 공유합니다.

예제 코드 #

제공된 Rust 코드는 웹 애플리케이션에서 주어진 방에 대한 댓글을 검색하는 두 개의 비동기 함수, find_comment_by_roomfind_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)
}
  1. find_comment_by_room: 이 함수는 HTTP 요청, RoomUrlDto가 포함된 JSON 본문, RoomQueryString이 포함된 쿼리 및 애플리케이션 상태를 입력 받습니다. 이 함수는 주어진 채팅방과 관련된 모든 댓글을 데이터베이스에서 찾습니다. 그런 다음 요청이 사용자로부터 온 것인지 확인합니다. 그렇지 않은 경우 댓글을 JSON 응답으로 반환합니다. 사용자의 요청인 경우 각 댓글에 대해 사용자의 comment_like를 검색하고 CommentResponse 객체를 구성한 후 JSON 응답으로 반환합니다.
  2. 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_roomfind_comment_by_room에 비해 상당히 향상된 성능을 달성할 수 있습니다. 처리해야 할 댓글이 많은 경우에 특히 그렇습니다. 이 동시성 기반 접근 방식은 데이터베이스 쿼리 실행 완료를 기다리는 전체 시간을 줄여 응답 시간을 빠르게 하고 웹 애플리케이션의 효율성을 높입니다.

초기 Postgres connection pool 생성 시간 #

이 섹션은 이 프로젝트를 진행하면서 가장 가치 있는 경험이었습니다. 여태껏, 동시성에 대한 고민은 시간을 절약하고 성능을 향상시킬 수 있다는 점을 언급했습니다만…

놀랍게도 초기 쿼리 실행의 경우, concurrent한 쿼리 실행은 순차 실행에 비해 훨씬 더 많은 시간이 걸릴 수 있습니다.

함수 이름초기 실행 시간초기 실행 후
find_comment_by_room620ms314ms
find_best_comment_by_room1313ms127ms

이 결과는 제 예상과는 달랐고, 굉장희 의아한 수치였는데요. 저는 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_CONNECTION1로 설정하면 그 성능은 다음과 같습니다.

함수 이름초기 실행 시간초기 실행 후
find_comment_by_room575ms560ms
find_best_comment_by_room666ms637ms

이는 이전 결과와 비교할 때 한 번에 여러 연결을 생성해야 하는 경우와 그다지 차이가 나지 않습니다.