Error Handling

RESTful API의.오류 처리는 개발자 경험과 시스템 안정성에 중요한 영향을 미치는 핵심 요소이다.
오류 상황을 어떻게 다루고 전달하는지에 따라 API의 품질이 크게 달라질 수 있다.

HTTP 상태 코드의 올바른 활용

HTTP 상태 코드는 클라이언트에게 요청 처리 결과를 전달하는 표준화된 방법이다.
오류 상황에서 적절한 상태 코드를 반환하는 것이 중요하다.

주요 오류 상태 코드 카테고리

상태 코드를 선택할 때는 상황에 가장 정확하게 맞는 코드를 사용해야 한다. 예를 들어, 사용자 인증이 필요한 경우 403(Forbidden)이 아닌 401(Unauthorized)을 사용하는 것이 적절하다.

오류 응답 본문의 구조화

상태 코드만으로는 오류에 대한 충분한 정보를 제공할 수 없다. 따라서 일관되고 유용한 오류 응답 본문을 구성하는 것이 중요하다.

일반적인 오류 응답 구조

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "status": 400,
  "code": "INVALID_PARAMETER",
  "message": "입력한 매개변수가 유효하지 않습니다.",
  "details": [
    {
      "field": "email",
      "message": "유효한 이메일 형식이 아닙니다."
    },
    {
      "field": "password",
      "message": "비밀번호는 최소 8자 이상이어야 합니다."
    }
  ],
  "timestamp": "2023-03-22T13:45:30Z",
  "requestId": "a1b2c3d4-e5f6-g7h8-i9j0"
}

주요 오류 응답 필드

이러한 구조는 개발자와 사용자 모두에게 유용한 정보를 제공한다. 특히 details 필드는 유효성 검사 오류와 같은 여러 문제가 있을 때 유용하다.

오류 코드 체계 수립

애플리케이션별 오류 코드를 체계적으로 관리하면 문제 해결이 용이해진다.

오류 코드 설계 방식

일관된 패턴을 사용하고 문서화하는 것이 중요하다.
오류 코드는 개발자가 문제를 빠르게 식별하고 디버깅할 수 있도록 도와준다.

국제화(i18n) 지원

글로벌 서비스의 경우, 오류 메시지를 다양한 언어로 제공하는 것이 중요하다.

국제화 지원 방법

  1. 클라이언트 주도 방식: 오류 코드만 전송하고 클라이언트에서 번역

    1
    2
    3
    4
    5
    
    {
      "status": 400,
      "code": "INVALID_EMAIL",
      "timestamp": "2023-03-22T13:45:30Z"
    }
    
  2. 서버 주도 방식: 클라이언트의 언어 설정에 따라 서버에서 번역

    1
    2
    3
    4
    5
    6
    
    {
      "status": 400,
      "code": "INVALID_EMAIL",
      "message": "유효한 이메일 주소를 입력해주세요.",
      "timestamp": "2023-03-22T13:45:30Z"
    }
    
  3. 하이브리드 방식: 기본 메시지와 함께 번역 키 제공

    1
    2
    3
    4
    5
    6
    7
    
    {
      "status": 400,
      "code": "INVALID_EMAIL",
      "message": "유효한 이메일 주소를 입력해주세요.",
      "messageKey": "validation.email.invalid",
      "timestamp": "2023-03-22T13:45:30Z"
    }
    

API의 사용 맥락과 클라이언트 유형에 따라 적절한 방식을 선택한다.

5. 보안 고려사항

오류 메시지는 보안에 영향을 줄 수 있으므로, 적절한 정보 노출 수준을 결정해야 한다.

오류 정보 노출 원칙

예를 들어, 데이터베이스 연결 오류의 경우 공개 API에서는 “서비스 일시적 오류"와 같은 일반적인 메시지를 반환하고, 내부적으로 로깅하는 것이 안전하다.

유효성 검사 오류 처리

API 요청에 대한 유효성 검사는 흔한 오류 원인이므로, 이에 대한 특별한 처리가 필요하다.

효과적인 유효성 검사 오류 전달 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "status": 422,
  "code": "VALIDATION_ERROR",
  "message": "입력 데이터 유효성 검사에 실패했습니다.",
  "details": [
    {
      "field": "username",
      "value": "a",
      "rule": "minLength",
      "message": "사용자 이름은 최소 3자 이상이어야 합니다."
    },
    {
      "field": "email",
      "value": "invalid-email",
      "rule": "format",
      "message": "유효한 이메일 형식이 아닙니다."
    }
  ]
}

이러한 구조적 응답은 클라이언트가 각 필드별 오류를 처리하기 쉽게 한다. 특히 폼 검증에 유용하다.

비즈니스 로직 오류 처리

비즈니스 규칙 위반과 같은 애플리케이션별 오류도 명확하게 전달해야 한다.

비즈니스 오류 응답 예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "status": 400,
  "code": "INSUFFICIENT_FUNDS",
  "message": "잔액이 부족합니다.",
  "details": {
    "availableBalance": 5000,
    "requiredAmount": 10000,
    "currency": "KRW"
  }
}

비즈니스 오류의 경우, 문제 해결을 위한 구체적인 정보를 details 필드에 포함시키는 것이 유용하다.

멱등성과 재시도 처리

네트워크 문제로 인한 재시도 시나리오를 고려한 오류 처리가 필요하다.

재시도 관련 오류 응답 예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "status": 409,
  "code": "DUPLICATE_REQUEST",
  "message": "동일한 요청이 이미 처리되었습니다.",
  "details": {
    "requestId": "a1b2c3d4",
    "completedAt": "2023-03-22T13:40:25Z",
    "originalTransactionId": "txn_12345"
  }
}

멱등성을 보장하는 API는 중복 요청을 감지하고, 이미 성공적으로 처리된 작업에 대한 정보를 제공해야 한다.

오류 로깅과 모니터링

오류 처리는 클라이언트 응답뿐만 아니라 서버 측 로깅과 모니터링도 중요하다.

효과적인 오류 로깅 전략

로그와 클라이언트 응답 사이의 일관성을 유지하는 것이 디버깅에 도움이 된다.

문서화와 개발자 지원

마지막으로, 오류 처리 방식을 명확하게 문서화하여 API 소비자가 예상할 수 있게 해야 한다.

오류 문서화 요소

OpenAPI(Swagger)와 같은 도구를 사용하여 오류 응답을 공식 API 문서의 일부로 포함시키는 것이 좋다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# OpenAPI 예시
responses:
  400:
    description: 잘못된 요청
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Error'
        examples:
          validation_error:
            summary: 유효성 검사 오류
            value:
              status: 400
              code: "VALIDATION_ERROR"
              message: "입력 데이터 유효성 검사에 실패했습니다."
              details: [...]

실제 구현 예시 (Node.js/Express)

오류 처리를 실제로 구현하는 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 오류 클래스 정의
class ApiError extends Error {
  constructor(statusCode, code, message, details = null) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
    this.timestamp = new Date().toISOString();
    this.requestId = `req_${Math.random().toString(36).substring(2, 12)}`;
  }
}

// 특정 오류 유형 정의
class ValidationError extends ApiError {
  constructor(details) {
    super(422, 'VALIDATION_ERROR', '입력 데이터 유효성 검사에 실패했습니다.', details);
  }
}

// Express 미들웨어로 오류 처리
function errorHandler(err, req, res, next) {
  console.error(err);
  
  // API 오류 인스턴스인 경우
  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      status: err.statusCode,
      code: err.code,
      message: err.message,
      details: err.details,
      timestamp: err.timestamp,
      requestId: err.requestId
    });
  }
  
  // 기타 처리되지 않은 오류
  return res.status(500).json({
    status: 500,
    code: 'INTERNAL_ERROR',
    message: '서버 내부 오류가 발생했습니다.',
    timestamp: new Date().toISOString(),
    requestId: `req_${Math.random().toString(36).substring(2, 12)}`
  });
}

// 라우트 핸들러 예시
app.post('/users', async (req, res, next) => {
  try {
    // 유효성 검사
    const errors = validateUser(req.body);
    if (errors.length > 0) {
      throw new ValidationError(errors);
    }
    
    // 비즈니스 로직
    const user = await createUser(req.body);
    
    // 성공 응답
    res.status(201).json(user);
  } catch (err) {
    next(err); // 오류 핸들러로 전달
  }
});

// 미들웨어 등록
app.use(errorHandler);

이 예시는 구조화된 오류 처리 방식을 보여주며, 다양한 오류 유형에 대해 일관된 응답을 생성한다.


용어 정리

용어설명

참고 및 출처