Rust에서 Async-GraphQL의 최적화된 오류 관리: 사용자 정의 오류 확장 및 친숙한 오류 메시지

Rust에서 GraphQL 서버를 개발할 때, async-graphql라는 강력한 서버 라이브러리를 사용하면 세심한 오류 관리가 가능합니다. 본 문서에서는 사용자 정의 오류 확장과 친숙한 오류 메시지를 통해 API 사용자에게 더 나은 경험을 제공하는 방법을 공유합니다. 또한 개발자들이 문제를 효과적으로 디버그할 수 있도록 지원하는 방법도 설명합니다.

GraphQL 오류 처리가 중요한 이유

GraphQL은 API 조회 언어로, 그 오류 응답 형식이 프런트 엔드 개발자 경험에直接影响을 미칩니다. GraphQL 표준 오류 구조는 message, locations, path 필드만을 포함하지만, 실제 응용에서는 오류 코드, 디버그 정보와 같은 더 많은 컨텍스트 정보가 필요합니다. async-graphql은 오류 확장(Error Extensions) 메커니즘을 통해 이 문제를 해결합니다. 이 기능을 통해 개발자는 오류 객체에 사용자 정의 메타데이터를 추가할 수 있습니다.

오류 확장 기초: ErrorExtensionValues

async-graphql은 ErrorExtensionValues 구조체를 통해 확장 정보를 저장합니다. 이 구조체는 키-값 쌍의 컬렉션입니다. 다음은 이 구조체의 핵심 구현입니다:

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct ErrorExtensionValues(BTreeMap<String, Value>);

impl ErrorExtensionValues {
    /// 확장 값을 설정합니다.
    pub fn set(&mut self, name: impl AsRef<str>, value: impl Into<Value>) {
        self.0.insert(name.as_ref().to_string(), value.into());
    }
    
    // 기타 메서드...
}

이 구조체를 통해 오류에 임의의 키-값 쌍 데이터를 추가할 수 있습니다. 이 데이터는 JSON으로 직렬화되어 GraphQL 응답의 extensions 필드에 포함됩니다.

快速 시작: 기본 오류 확장 구현

오류에 확장 정보를 추가하는 가장 간단한 방법은 extend_with 메서드를 사용하는 것입니다. 다음은 분해 중입력 시 오류 코드를 추가하는 예입니다:

use async_graphql::*;
use std::num::ParseIntError;

struct Query;

#[Object]
impl Query {
    async fn parse_with_extensions(&self, input: String) -> Result<i32> {
        Ok("234a"
            .parse()
            .map_err(|err: ParseIntError| err.extend_with(|_, e| e.set("code", 400)))?)
    }
}

해석 오류가 발생할 때, 클라이언트는 다음 응답을 받을 수 있습니다:

{
  "errors": [
    {
      "message": "invalid digit found in string",
      "locations": [{"line": 1, "column": 1}],
      "path": ["parseWithExtensions"],
      "extensions": {
        "code": 400
      }
    }
  ]
}

고급 팁: 사용자 정의 오류 유형에 대한 ErrorExtensions 구현

자주 사용되는 오류 유형에 대해 직접 ErrorExtensions 트레이트를 구현하는 것이 좋습니다. 중복된 코드를 피울 수 있습니다. 예를 들어, 사용자 정의 오류 열거형을 만들면:

use async_graphql::*;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum MyError {
    #[error("리소스가 없음")]
    NotFound,
    
    #[error("서버 오류: {0}")]
    ServerError(String),
}

impl ErrorExtensions for MyError {
    fn extend(&self) -> Error {
        Error::new(format!("{}", self)).extend_with(|err, e| 
            match self {
                MyError::NotFound => e.set("code", "NOT_FOUND"),
                MyError::ServerError(reason) => e.set("reason", reason.clone()),
            })
    }
}

이제 해석기에서 이 오류 유형을 바로 사용할 수 있습니다:

#[Object]
impl Query {
    async fn get_resource(&self, id: ID) -> Result<Resource> {
        let resource = Resource::find_by_id(id)
            .ok_or(MyError::NotFound)?;
        Ok(resource)
    }
}

리소스가 없을 때, 클라이언트는 확장 정보를 포함한 오류 응답을 받을 수 있습니다:

{
  "errors": [
    {
      "message": "리소스가 없음",
      "locations": [{"line": 1, "column": 1}],
      "path": ["getResource"],
      "extensions": {
        "code": "NOT_FOUND"
      }
    }
  ]
}

오류 체인 확장: 여러 오류 정보 결합

async-graphql은 extend_with 메서드를 체인식으로 호출할 수 있습니다. 이는 여러 정보源에서 오류를 결합할 때 유용합니다:

async fn parse_with_chained_extensions(&self) -> Result<i32> {
    match "234a".parse() {
        Ok(n) => Ok(n),
        Err(e) => Err(e
            .extend_with(|_, e| e.set("code", 404))
            .extend_with(|_, e| e.set("details", "입력 형식이 잘못되었습니다"))
            .extend_with(|_, e| e.set("retryable", false))),
    }
}

최종적으로 생성되는 오류 확장은 모든 설정된 키-값 쌍을 포함하며, 나중에 설정된 키는 이전의 키를 덮어씁니다.

친숙한 오류 메시지 디자인 원칙

기술적 구현 이외에도 친숙한 오류 메시지를 디자인하는 것도 중요합니다. 다음은 실용적인 제언입니다:

  1. 사용자용 메시지: 기술 용어를 피하고 간결한 언어로 문제를 설명합니다.
  2. 일관된 오류 코드: 오류 코드 체계(예: NOT_FOUND, INVALID_INPUT)를 정립하여 프런트 엔드에서 처리를 용이하게 합니다.
  3. 디버그 정보 분리: 민감한 디버그 정보는 확장 필드를 통해 제공하고, 일반 사용자는 이러한 정보를 보지 않습니다.
  4. 작업 제안: 오류 메시지에 문제 해결 방법을 구체적으로 제시합니다.

예를 들어, "ParseIntError" 대신 "입력은 유효한 정수여야 합니다. 형식을 확인한 후 다시 시도하세요"와 같이 표현합니다.

공통 함정 및 해결 방법

async-graphql 오류 확장 사용 시 주의할 점有几个:

Rust 트레이트 구현 제한

Rust는 안정적인 트레이트 특화를 지원하지 않습니다. 일부 유형에 대해 직접 ErrorExtensions를 구현하면 컴파일 오류가 발생할 수 있습니다. 예를 들어:

// 이는 컴파일 오류를 유발합니다
async fn will_not_compile(&self) -> Result<i32> {
    "234a".parse().extend_err(|_, e| e.set("code", 404))
}

해결 방법은 오류 값을 명시적으로 참조하는 것입니다:

async fn will_compile(&self) -> Result<i32> {
    "234a"
        .parse()
        .map_err(|ref e: ParseIntError| e.extend_with(|_, e| e.set("code", 404)))
}

확장 키 이름 충돌

extend_with를 체인식으로 호출할 때, 동일한 키 이름을 사용하면 나중에 설정된 값이 이전의 값을 덮어씁니다. 팀 내에서 확장 키 이름 규범을 정립하여 의도치 않은 덮어쓰기 상황을 피하세요.

요약 및 최선의 실천

async-graphql은 유연하고 강력한 오류 처리 메커니즘을 제공합니다. 사용자 정의 오류 확장을 통해 사용자 친화적인 GraphQL 서비스를 구축할 수 있습니다.以下是 키폭수실천:

  • 모든 사용자ISIBLE 오류에 확장 정보를 추가하고 최소한 오류 코드를 포함시킵니다.
  • 사용자 정의 오류 유형에 ErrorExtensions 트레이트를 구현하여 중복 코드를 줄입니다.
  • extend_err 메서드를 사용하여 오류 처리 코드를 간결화합니다.
  • 명확한 오류 메시지를 설계하고 사용자 메시지와 디버그 정보를 구분합니다.
  • 팀 내에서 일관된 오류 코드와 확장 필드 규범을 정립합니다.

이러한 실천을 통해 GraphQL API가 사용자에게 더 나은 개발자 경험을 제공하고 프런트 엔드 오류 처리 로직을 간결화할 수 있습니다. 자세한 내용은 공식 문서docs/en/src/error_extensions.md를 참조하세요.

태그: Rust async-graphql error-handling web-development GraphQL

6월 19일 23:43에 게시됨