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

댓글이

·811 자

서론 #

Datguri는 사용자가 원하는 모든 페이지에 댓글을 남길 수 있는 방법을 제공하는 크롬 확장 프로그램입니다.

이 확장 프로그램은 크롬 컴퓨터 환경에서 사용 가능하며, 저는 RustActix 를 사용하여 애플리케이션의 백엔드를 담당했습니다.

😍 사용 방법은? 😍

  1. 궁금한 것이 있다면 웹사이트에 댓글을 남기세요!
  2. 공감가는 댓글을 발견하면 ‘좋아요’ 버튼을 클릭하여 인기 댓글로 만드세요!
  3. 다른 페이지에도 댓글을 남겨 인기 있는 글쓴이가 되어 보세요!

이 프로젝트를 구축하면서 마주친 몇 가지 기술적 도전 과제 중 하나는 Rust와 Actix에 익숙하지 않았다는 것입니다. 그리고, 프론트엔드 개발자들의 어려웠던 점은 크롬 확장 프로그램 개발은 일반 웹 애플리케이션 개발과는 또 사뭇 달랐습니다.

폴더 구조 #

.
├── Cargo.toml
├── Dockerfile
├── app
│   ├── Cargo.toml
│   └── src
│       ├── controller
│       ├── main.rs
│       └── usecase
├── entity
│   ├── Cargo.toml
│   └── src
│       ├── comment.rs
│       ├── lib.rs
│       ├── reply.rs
│       ├── report.rs
│       └── user.rs
├── storage
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       └── postgres
└── utils
    ├── Cargo.toml
    └── src
        └── lib.rs

12 directories, 14 files

프로젝트 소스 코드는 크게 4 부분으로 나뉩니다.

  1. app : 이 부분은 프로젝트의 주요 부분으로, HTTP 요청과 응답을 처리하는 controller*와 비즈니스 로직을 담당하는 usecase를 포함합니다.
  2. entity : 프로젝트의 데이터 구조를 포함하는 부분으로, 비즈니스 로직에서 데이터 구조를 분리하려고 시도했습니다. 또한 MSA (마이크로서비스 아키텍처)의 가능성 때문에 별도의 부분으로 만들려고 했습니다.
  3. storage : 데이터 저장을 담당하는 부분으로, 데이터베이스로 postgres를 사용하고 데이터베이스 드라이버로 sqlx를 사용했습니다.
  4. utils : 프로젝트에서 사용되는 공통 기능을 포함하는 부분입니다.

각 폴더는 자체 Cargo.toml 파일을 가지고 있으며, Rust Cargo에서 지원하는 workspace 기능을 사용하여 통합되었습니다.

기술 스택 #

  1. 프론트엔드: Typescript, ReactJS
  2. 백엔드: Rust, Actix, PostgresQL
  3. CI/CD: Github Actions, Docker, Wrangler CLI
  4. 클라우드: Terraform, AWS, Cloudflare
  5. 기타: Haproxy

백엔드 개발자로서 저는 Rust를 사용하여 API 서버를 구축하고 서버 유지 관리 및 CI/CD 파이프라인 제작을 맡았었습니다.

제가 배운 점 #

1. Rust에서의 JWT 검증 #

    #[derive(Serialize, Deserialize)]
    struct Claims {
        sub: String,
        iat: usize,
        exp: usize,
    }

    /// Header의 Authorization 필드에서 jwt의 user_id 찾기
    pub async fn validate_request(
        req: HttpRequest,
        pool: &Pool<Postgres>,
    ) -> Result<String, HttpResponse> {
        // 요청 헤더에서 access_token 추출
        let access_token: String = match req.headers().get("Authorization") {
            Some(token) => match token.to_str() {
                Ok(token) => token.replace("Bearer ", ""),
                Err(_) => {
                    return Err(HttpResponse::InternalServerError().json(JsonMessage {
                        msg: "Error parsing header to string".to_string(),
                    }));
                }
            },
            None => {
                return Err(HttpResponse::InternalServerError().json(JsonMessage {
                    msg: "Authorization field not exist".to_string(),
                }));
            }
        };

        // 데이터베이스에서 사용자 찾기
        let user: Option<User> = match validate_jwt(access_token) {
            Ok(sub) => match find_user_by_email(pool, &sub).await {
                Ok(user) => user,
                Err(e) => {
                    return Err(HttpResponse::InternalServerError().json(format!("Error: {:?}", e)));
                }
            },
            // JwtError
            Err(e) => {
                return Err(HttpResponse::Unauthorized().json(JsonMessage {
                    msg: format!("{:?}", e),
                }));
            }
        };

        // 유효하다면 이메일 반환
        return if let Some(user) = user {
            Ok(user.uuid)
        } else {
            Err(HttpResponse::Unauthorized().json(JsonMessage {
                msg: "User not found".to_string(),
            }))
        };
    }

위 코드 조각에서 볼 수 있듯이, validate_request라는 Rust 함수는 HTTP 요청과 연결 풀을 입력으로 받고 사용자의 UUID 또는 오류 응답을 반환합니다. 함수의 작업 흐름은 다음과 같습니다:

  1. 요청에서 “Authorization” 헤더의 액세스 토큰을 추출합니다.
  2. JWT를 검증하고 사용자의 이메일(“sub” claim)을 얻습니다.
  3. 데이터베이스에서 해당 이메일을 사용하여 사용자를 조회합니다.
  4. 사용자를 찾은 경우 그들의 UUID를 반환하고, 그렇지 않은 경우 “Unauthorized” 오류를 반환합니다.

코드가 다른 언어의 유사한 구현에 비해 불필요하게 복잡해 보이지만, 이는 Rust의 안전성과 명시적인 오류 처리에 대한 강조 때문이라고 생각합니다. String 처리와 같이 간단한 작업을 수행하는 데 보다 더 많은 노력이 필요했지만, Rust가 제공하는 memory-safe 철학을 충족하는 애플리케이션을 구축하는 데에는 필수불가결한 요소였다고 생각하고 있습니다.

2. Concurrent한 데이터베이스 쿼리 실행 #

이 주제에 대해서는 좀 더 자세한 기술되어 있는 글이 따로 있습니다. 아래 게시물을 읽고 이 지점으로 돌아오는 것을 추천합니다.

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

아무래도 저의 관심사 중 하나는 애플리케이션의 성능을 최적화하여 가능한 최고의 사용자 경험을 제공하는 것이였습니다. 동시성은 여러 작업을 동시에 실행할 수 있게 하여 효율성을 향상시키는 가장 중요한 고려사항 중 하나이며, 이 경우에는 특히 데이터베이스 쿼리 실행의 맥락에서 그랬습니다.

위 글에 나온 코드는 주어진 채팅방에 대한 댓글을 검색하는 두 가지 다른 접근 방식을 보여줍니다. find_comment_by_roomfind_best_comment_by_room 두 함수는 동일한 목표를 공유하지만 comment_likes 처리 방식에서 차이가 있습니다. 첫 번째 함수는 comment_likes에 대해 데이터베이스를 순차적으로 쿼리하는 간단한 for 루프를 사용하는 반면, 두 번째 함수는 join_all 함수를 사용하여 여러 데이터베이스 쿼리를 한 번에 동시에 실행하도록 Rust의 futures 라이브러리를 활용합니다.

동시 실행이 완료된 후, 결과를 처리하고 각 댓글에 대한 CommentResponse 객체를 구성합니다. 이를 통해 최종 결과를 JSON 응답으로 효율적으로 반환할 수 있습니다.

이번에 작성한 Rust 웹 애플리케이션의 경우, 동시성을 활용함으로써 데이터베이스 쿼리 실행의 성능을 향상시켜 애플리케이션의 응답성을 높일 수 있었습니다.

Conclusion #

결론적으로, Rust를 사용하여 웹 애플리케이션을 개발하는 경험은 언어의 안전성, 동시성 및 스레드 관리에 대해 많은 것을 배울 수 있게 해주었습니다. 그러나 Rust가 단순한 백엔드 API 서버를 생성하는 데 이상적인 선택이 아닐 수도 있음이 분명해졌습니다. 다른 언어들에 비해 memory-safety 및 performance을 보장하기 위해 필요한 노력이 상당히 많았습니다. Rust는 안전성과 성능이 최우선인 low-level 시스템 프로그래밍 작업에 더 적합한 것으로 보입니다. 그럼에도 불구하고 Rust를 사용하면 동시성의 세계에 더 깊이 뛰어들어 그 이점을 이해하고 애플리케이션의 성능을 향상시키는 데 도움이 될 수 있습니다. 많은 어려움에도 불구하고 저는 Rust가 제공하는 엄격함을 충족하는 코딩 과정 자체는 굉장히 즐거웠으며, 이후의 경험하게 될 프로젝트에서도 이번에 배운 점이 많이 도움이 될 것이라고 확신합니다.