본문 바로가기

예외 처리와 필터

빌트인 HTTP 예외와 사용자 정의 예외를 발생시키고 예외 필터로 오류 응답 형식과 처리 범위를 일관되게 만듭니다.

웹 애플리케이션을 만들다 보면 없는 리소스에 접근하거나, 데이터베이스가 실패하거나, 사용자 입력이 규칙을 어기는 등 여러 예외(exception) 상황을 만납니다. 이런 예외를 제대로 다루지 않으면 사용자에게 알아보기 어려운 오류 메시지가 그대로 보이거나 애플리케이션이 비정상적으로 끝날 수 있습니다.

NestJS는 예외를 HTTP 응답으로 바꾸는 기본 처리와, 그 응답을 원하는 형식으로 다듬는 예외 필터(exception filter)를 제공합니다. 이 글에서는 내장 HTTP 예외를 던지는 방법, 예외 필터를 만들고 적용하는 범위, 사용자 정의 예외를 차례로 다룹니다.

Nest 예외 응답 · 기본 예제와 확장

HttpException 응답 예제에서 공개 계약과 비공개 진단을 확장할 수 있습니다

본문의 @Catch(HttpException)은 statusCode·timestamp·path·message를 응답합니다. 아래 catch-all·도메인 오류 분류·요청 ID·비공개 로그는 추가로 설계할 때의 제안이며 본문에 구현되어 있지는 않습니다.

확장 설계: 공개 응답과 비공개 진단을 분리한다 원문의 HttpException 전용 필터를 확장하여 HTTP 예외·독립 도메인 오류·unknown을 분류하고 공개 응답과 비공개 진단을 나누는 제안이다. HTTP EXCEPTION 상태 · errorCode 보존 DOMAIN ERROR 운송 계층과 독립 UNKNOWN 예상하지 못한 실패 @Catch() · ArgumentsHost 한 번만 분류하고 매핑 원래 상태 · 도메인 매핑 · 500 PUBLIC CONTRACT status · code message · requestId stack · SQL · 비밀 값 제외 PRIVATE DIAGNOSTICS 원인 · 스택 · 요청 문맥 requestId로 공개 응답과 연결 CLIENT 응답에 따른 후속 행동 OPERATOR 원인 진단 · 경보 · 복구
오류 분류 뒤 공개 응답과 비공개 진단을 분리한다 확장 설계에서 HTTP 예외, 독립 도메인 오류, 알 수 없는 오류를 catch-all 필터가 분류한다. 공개 응답은 안전한 상태·코드·메시지·요청 ID로 클라이언트의 후속 판단을 돕고, 비공개 로그는 같은 요청 ID로 운영자의 원인 진단에 연결한다. 본문 HttpException 전용 필터만으로 구현된 기능은 아니다. HTTP 예외원래 상태 · 오류코드 도메인 오류전송 계층과 독립 알 수 없는 오류예상하지 못한실패 catch-all ·확장 설계한 번 분류 · 상태매핑HTTP 상태 /도메인 / 500 공개 응답status ·codemessage·requestId비밀 값 ·스택 제외 비공개 진단원인 ·스택 ·문맥같은requestId구조화된로그 클라이언트응답별후속 행동재시도는별도 정책 운영자진단 ·경보 ·복구공개응답과추적
  1. 실패 발생

    HTTP 예외, 운송 계층과 독립적인 도메인 오류, 예상하지 못한 오류를 구분합니다.

  2. catch-all 필터

    ArgumentsHost에서 HTTP 요청을 꺼내 상태와 안정적인 오류 코드를 한 번만 매핑합니다.

  3. 공개 응답

    상태·오류 코드·안전한 메시지·요청 ID만 보내 클라이언트가 다음 행동을 고르게 합니다.

  4. 비공개 진단

    원인과 스택은 구조화 로그에 남기고 같은 요청 ID로 운영자가 추적합니다.

필터는 route → controller → global 순으로 가장 가까운 범위부터 탐색한다. DI가 필요한 전역 필터는 module provider의 APP_FILTER로 등록합니다. @Catch(HttpException)만 붙은 본문 필터는 일반 Error를 잡는 catch-all이 아닙니다. 응답 이후의 재시도·복구는 별도 정책입니다.

그림은 이 글의 기본 예제를 넘어선 확장 설계입니다. HTTP 예외, 전송 계층과 독립된 도메인 오류, 예상하지 못한 오류를 모든 예외를 잡는 필터가 한 번 분류하고, 클라이언트에게는 상태, 오류 코드, 안전한 메시지, 요청 ID만 담은 공개 응답을, 운영자에게는 원인과 스택을 담은 비공개 진단 로그를 같은 요청 ID로 연결해 남깁니다. 본문의 @Catch(HttpException) 필터는 일반 Error를 잡지 않으며, 요청 ID와 비공개 로그도 아직 구현하지 않았습니다. 맨 아래 줄은 필터를 라우트, 컨트롤러, 전역 순서로 찾고, DI가 필요한 전역 필터는 APP_FILTER로 등록한다는 점을 짚습니다.


내장 HTTP 예외

NestJS는 표준 HTTP 예외를 위한 내장 예외 계층을 제공합니다. @nestjs/common의 HttpException 클래스와 그 하위 클래스인 NotFoundException, BadRequestException, UnauthorizedException 등이 여기에 속합니다. 컨트롤러나 서비스에서 조건이 맞지 않을 때 이 예외를 throw하면, NestJS가 그것을 받아 알맞은 상태 코드와 메시지를 담은 HTTP 응답을 돌려줍니다.

src/items/items.controller.ts
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { ItemsService } from './items.service';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    const item = this.itemsService.findItemById(id); // 예를 들어, 서비스에서 아이템을 찾는다고 가정
    if (!item) {
      // 아이템이 없는 경우 NotFoundException을 발생시킵니다.
      throw new NotFoundException(`Item with ID "${id}" not found.`);
    }
    return item;
  }
}

findOne은 주어진 id의 항목을 찾고, 없으면 NotFoundException을 던집니다. NestJS는 이 예외를 받아 404 상태 코드와 지정한 메시지를 응답합니다. 개발자가 일일이 res.status(404).json(...)을 쓰지 않고, 선언적으로 예외를 다룰 수 있습니다.

이 예제를 따라 하려면 생략된 ItemsService.findItemById()가 항목이나 undefined를 동기적으로 돌려줘야 하고, 컨트롤러와 서비스를 같은 모듈에 등록해야 합니다. 예를 들어 ID '1'이면 { id: '1', name: 'Notebook' }을, 다른 ID면 undefined를 돌려주게 준비하면 정상 조회와 404를 비교할 수 있습니다. DB 조회처럼 Promise를 돌려주는 서비스로 바꾼다면 핸들러도 async로 바꾸고 결과를 await한 뒤 검사해야 합니다. 아직 끝나지 않은 Promise를 if (!item)으로 검사하면 빠진 항목을 가려낼 수 없습니다.

NestJS 12부터는 예외를 던질 때 기계가 읽을 수 있는 오류 코드를 함께 넘길 수 있습니다. 예외 필터 문서에 따르면 new BadRequestException('Password is too weak', { errorCode: 'WEAK_PASSWORD' })처럼 errorCode를 넘기면, 기본 예외 처리가 응답 본문에 message, error, statusCode와 함께 errorCode를 넣습니다. 사람이 읽는 메시지를 파싱하지 않고도 클라이언트가 실패의 종류를 구분할 수 있게 해 주는 기능입니다. 반면 cause는 로깅을 위한 값이라 응답에 직렬화되지 않습니다.

더 깊이: 기본 예외 처리가 로그에 남기는 오류와 남기지 않는 오류

기본 예외 처리는 HttpException과 그 하위 클래스를 로그에 남기지 않습니다. 예외 필터 문서는 이런 예외를 정상적인 애플리케이션 흐름의 일부로 보며, HttpException이 상속하는 IntrinsicException의 인스턴스는 내장 필터가 기록하지 않는다고 설명합니다. 그래서 404나 409가 아무리 많이 나도 터미널에는 아무것도 찍히지 않습니다. 반대로 그 밖의 오류는 ExceptionsHandler라는 이름의 로거로 기록한 뒤, 응답으로는 statusCode: 500과 message: 'Internal server error'만 보냅니다.

모르는 오류가 모두 500이 되는 것은 아닙니다. 기본 처리는 http-errors 라이브러리가 만드는 모양의 오류, 즉 statusCode와 message를 가진 오류를 부분적으로 알아보고 그 값으로 응답합니다. NestJS 12.2부터는 그런 오류의 expose가 false이면 메시지 대신 Bad Gateway 같은 상태 코드의 표준 문구를 보내는데, http-errors는 500 이상의 상태 코드에서 expose를 false로 정합니다. 결국 같은 400 응답이라도 BadRequestException은 기록되지 않고 http-errors 모양의 오류는 기록됩니다. 실패를 빠짐없이 집계하려면 기본 로그에 기대지 말고 필터나 인터셉터에서 직접 기록해야 합니다.


예외 필터

내장 처리만으로 부족해 특정 예외에 정해진 응답 형식을 주거나 로깅을 더하고 싶을 때는 예외 필터를 씁니다. 이 글의 HTTP 예외 필터는 요청 처리 경로에서 처리되지 않은 예외에 응답 로직을 적용하며, 컨트롤러나 서비스에서 반환하거나 await한 Promise의 실패도 이 경로에 포함됩니다. 요청과 따로 시작한 비동기 작업이나 타이머의 오류까지 잡아 주는 프로세스 전체의 오류 처리기는 아닙니다.

예외 필터는 @Catch() 데코레이터에 처리할 예외 종류를 적습니다. 예외 클래스를 하나 또는 여러 개 넘길 수 있고, 인자가 없으면 모든 예외를 처리합니다. 필터 클래스는 ExceptionFilter 인터페이스를 구현하며, catch() 메서드가 발생한 예외 객체와 ArgumentsHost를 인자로 받습니다. 적용은 @UseFilters()로 컨트롤러나 메서드에 하거나, main.ts에서 전역으로 합니다.

클라이언트에게 더 자세하고 일관된 오류 응답을 주는 필터를 만들어 봅니다.

src/common/filters/http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import type { Request, Response } from 'express';

@Catch(HttpException) // HttpException과 그 하위 클래스를 잡습니다.
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus(); // 예외의 HTTP 상태 코드

    // HttpException에 담긴 공개 응답 메시지를 가져옵니다.
    const errorResponse = exception.getResponse();
    const errorMessage = typeof errorResponse === 'string'
      ? errorResponse
      : (errorResponse as any).message || 'An unexpected error occurred.';

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: errorMessage, // 클라이언트에게 보여 줄 메시지
      // errorCode: exception.errorCode, // NestJS 12의 오류 코드를 유지하려면 직접 넣습니다.
    });
  }
}

이 필터는 적용 범위 안의 HttpException에 timestamp와 path를 더해 응답합니다. getResponse()는 문자열이나 객체를 돌려줄 수 있고, 객체의 message가 배열이면 이 코드도 배열을 그대로 응답에 넣습니다. 반면 응답을 직접 새로 만들기 때문에, 원래 응답 객체에 있던 다른 속성이나 NestJS 12의 errorCode는 새 JSON에 복사하지 않으면 사라집니다. 클라이언트가 message를 늘 문자열로 가정하거나 원래 오류 객체가 모두 보존된다고 가정해서는 안 되며, 공개 오류 코드를 유지하려면 주석처럼 exception.errorCode를 응답 형식에 명시해야 합니다.

그림의 일반 오류 분류, 비공개 진단 로그, 요청 ID는 확장 설계이며, 지금의 필터는 그 기능을 구현하지 않습니다. 특히 DB 오류 메시지나 스택을 그대로 공개 응답에 복사하지 말고, 클라이언트용 메시지와 운영자용 진단 정보를 나눠야 합니다. path: request.url에는 쿼리 문자열도 들어갈 수 있으므로, 공개 경로에 어떤 정보를 담을지도 정합니다. NestJS 12.3의 HttpException 소스에서 문자열과 객체 응답, errorCode를 다루는 방식을 확인할 수 있습니다.

앞의 서비스 준비에서 없는 ID '9'를 조회한다고 해 봅시다. 전역 접두사가 있다면 GET /api/items/9입니다. 메서드에 @UseFilters(HttpExceptionFilter)를 적용하면 상태 404는 그대로 두고, 응답 본문에 statusCode: 404, 실제 요청의 path, 매번 새로 만든 timestamp, Item with ID "9" not found.라는 message가 들어갑니다. 필터는 실패를 성공으로 바꾸지 않고 오류 응답의 형식을 정할 뿐이며, 처리 시각은 요청마다 다르므로 고정된 문자열로 비교하지 않습니다.


예외 필터의 적용 범위

필터는 메서드, 컨트롤러, 전역 세 범위에 적용할 수 있습니다. 메서드에 적용하면 그 메서드에서 난 HttpException에만 쓰입니다.

src/items/items.controller.ts
import { Controller, Get, Param, NotFoundException, UseFilters } from '@nestjs/common';
import { ItemsService } from './items.service';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @UseFilters(HttpExceptionFilter) // 이 메서드에서 발생하는 HttpException에만 적용
  @Get(':id')
  findOne(@Param('id') id: string) {
    const item = this.itemsService.findItemById(id);
    if (!item) {
      throw new NotFoundException(`Item with ID "${id}" not found.`);
    }
    return item;
  }
}

위 코드에서는 @UseFilters()를 붙인 findOne()만 필터를 거칩니다. 같은 컨트롤러에 메서드를 더 추가해도, 그 메서드에서 난 예외는 Nest의 기본 형식으로 응답합니다. 컨트롤러에 적용하면 그 컨트롤러의 모든 경로에 쓰입니다. 아래처럼 데코레이터를 클래스 선언 위로 옮기면 되며, 메서드 본문은 앞과 같으므로 생략했습니다.

src/items/items.controller.ts
import { Controller, Get, Param, NotFoundException, UseFilters } from '@nestjs/common';
import { ItemsService } from './items.service';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';

@UseFilters(HttpExceptionFilter) // 이 컨트롤러의 모든 라우트에 적용
@Controller('items')
export class ItemsController {
  // ...
}

애플리케이션 전체에 같은 오류 응답 형식이 필요하다면 전역으로 등록합니다. 특정 경로에만 다른 형식이 필요하다면 메서드나 컨트롤러 범위를 고릅니다. 앞 장의 main.ts를 이어서 쓴다면 ValidationPipe, api 전역 접두사, PORT 설정은 그대로 두고 필터의 import와 app.useGlobalFilters(...)만 더합니다.

src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.setGlobalPrefix('api');

  // 전역 예외 필터 등록
  // new로 만든 필터는 의존성 주입을 받을 수 없습니다.
  app.useGlobalFilters(new HttpExceptionFilter());

  await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();

같은 예외에 여러 범위의 필터가 후보라면 메서드, 컨트롤러, 전역 순서로 가장 가까운 필터를 찾습니다. 메서드 필터가 이미 처리한 예외가 컨트롤러와 전역 필터로 차례차례 넘어가지는 않습니다. 또 핸들러가 try/catch로 예외를 잡고 정상 값을 돌려줬다면 예외 필터는 실행되지 않습니다. 공통 로깅을 전역 필터에만 두면 지역 필터가 처리한 오류를 놓칠 수 있다는 점도 고려합니다. 요청 수명 주기 문서가 이 범위를 설명합니다.

app.useGlobalFilters(new HttpExceptionFilter())처럼 넘기는 방식은 의존성이 없는 지금의 필터에는 쓸 수 있지만, 필터 안에서 로깅 서비스 같은 프로바이더를 주입받아야 한다면 문제가 됩니다. 그럴 때는 APP_FILTER 토큰으로 모듈에 등록하면 DI를 쓰는 전역 필터가 되며, 같은 전역 적용을 위해 @UseFilters()를 추가할 필요는 없습니다.

src/app.module.ts (전역 필터로 등록 시 의존성 주입이 필요한 경우)
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
    // ... 다른 프로바이더들
  ],
  // ...
})
export class AppModule {}

이렇게 등록하면 HttpExceptionFilter의 인스턴스를 Nest가 직접 만들므로 생성자로 프로바이더를 주입받을 수 있습니다. 또 APP_FILTER로 등록한 필터는 어느 모듈의 providers에 두든 애플리케이션 전체에 적용됩니다. 그래서 보통은 필터가 정의된 곳과 가까운 모듈이나 루트 모듈 한 곳에만 둡니다.


사용자 정의 예외

HttpException을 상속해 애플리케이션의 업무 상황에 맞는 사용자 정의 예외를 만들 수도 있습니다. 코드를 읽기 쉬워지고 특정 예외 상황을 분명하게 표현할 수 있습니다.

src/common/exceptions/user-already-exists.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class UserAlreadyExistsException extends HttpException {
  constructor(username: string) {
    super(`User with username "${username}" already exists.`, HttpStatus.CONFLICT); // 409 Conflict
  }
}

super()의 첫 인자는 응답 메시지이고 두 번째 인자는 상태 코드이므로, 이 예외는 409 Conflict로 응답됩니다. 이 예외를 서비스에서 던지면, HttpException의 하위 타입이므로 HttpExceptionFilter가 처리합니다. 아래 서비스는 메모리 배열에 testuser를 미리 넣어 두었으므로 createUser('testuser')를 호출하면 바로 이 예외가 던져집니다. 필터가 적용된 범위라면 응답의 message는 User with username "testuser" already exists.가 됩니다.

src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserAlreadyExistsException } from '../common/exceptions/user-already-exists.exception';

@Injectable()
export class UsersService {
  private users: string[] = ['testuser'];

  createUser(username: string): string {
    if (this.users.includes(username)) {
      throw new UserAlreadyExistsException(username);
    }
    this.users.push(username);
    return `User ${username} created successfully.`;
  }
}

사용자 정의 예외를 쓰면 서비스 코드에는 도메인의 의미가 남고, 필터는 그것을 클라이언트에게 보낼 응답 형식으로 바꿉니다. 예외 필터를 설계할 때는 예외를 잡는 데서 끝내지 말고, 클라이언트가 재시도, 로그인 화면 이동, 입력 수정 같은 다음 행동을 고를 수 있도록 응답 형식을 정해 둡니다.


정리

  • 내장 HttpException과 그 하위 클래스를 던지면 NestJS가 알맞은 상태 코드와 메시지로 응답하며, NestJS 12부터는 errorCode로 기계가 읽을 수 있는 오류 코드를 함께 보낼 수 있습니다.
  • 예외 필터는 @Catch()로 처리할 예외를 고르고, 응답을 직접 만들 때는 보존할 필드(errorCode 등)를 명시해야 합니다.
  • 필터는 메서드, 컨트롤러, 전역 순서로 가장 가까운 것 하나만 실행되며, DI가 필요한 전역 필터는 APP_FILTER로 등록합니다.
  • @Catch(HttpException) 필터는 일반 Error를 잡지 않으며, 공개 응답과 내부 진단 정보는 나눠서 다룹니다.

연습 문제

1. 필터가 잡는 예외

HttpExceptionFilter를 전역으로 등록했습니다. 서비스가 UserAlreadyExistsException('testuser')를 던진 경우와, 일반 Error('database unavailable')를 던진 경우에 이 필터가 각각 실행되나요?

해설 보기

첫 예외는 HttpException의 하위 타입이므로 이 필터가 처리하고, 원래의 409 상태를 유지합니다.

일반 Error는 @Catch(HttpException)의 대상이 아니므로 이 필터가 처리하지 않습니다. 맞는 다른 필터가 없다면 기본 예외 처리가 500으로 응답합니다. 일반 오류까지 분류하려면 모든 예외를 잡는 별도의 정책을 설계해야 하며, 공개 응답에 내부 DB 메시지를 그대로 넣을지도 그때 따로 판단합니다.

2. Promise를 그대로 검사하면

findItemById()를 Promise<Item | undefined>를 돌려주도록 바꿨고, 결과는 undefined입니다. 기존의 동기 핸들러를 그대로 두면 if (!item)이 빠진 항목을 찾아내나요? 올바른 검사 순서를 쓰세요.

해설 보기

Promise 객체 자체는 참으로 평가되므로 if (!item)이 실행되지 않고, 핸들러는 Promise를 그대로 반환합니다.

핸들러를 async로 바꾸고 const item = await this.itemsService.findItemById(id)로 결과를 받은 다음 검사해야 합니다. 결과가 undefined이면 NotFoundException을 던지며, Promise가 실패하면 그 실패 역시 반환하거나 기다린 요청 처리 경로에서 예외 필터로 넘어갑니다.

3. 응답 객체에서 남는 필드

HttpExceptionFilter가 new BadRequestException(['name is required'], { errorCode: 'INVALID_NAME' })로 만든 예외를 받습니다. 요청 URL은 /users입니다. 응답에 남는 message, 상태, 경로와 사라지는 필드를 구분하세요. 필터 없이 기본 예외 처리가 응답했다면 errorCode는 어떻게 되었을까요?

해설 보기

BadRequestException은 배열 메시지로 { message: ['name is required'], error: 'Bad Request', statusCode: 400, errorCode: 'INVALID_NAME' }이라는 응답 객체를 만듭니다. 필터는 그 가운데 message만 꺼내므로 ['name is required'] 배열이 그대로 남고, JSON의 statusCode는 400, path는 /users, timestamp는 처리한 시각입니다. error와 errorCode는 복사하지 않으므로 응답에서 사라집니다.

필터 없이 기본 예외 처리가 응답했다면 이 응답 객체가 그대로 본문이 되어 errorCode: 'INVALID_NAME'이 들어갔을 것입니다. 사용자 정의 필터로 응답을 직접 만들면, 기본 처리가 넣어 주던 필드를 지킬지 스스로 정해야 합니다.

4. 여러 범위의 필터와 try/catch

같은 HttpException을 처리하는 메서드, 컨트롤러, 전역 필터를 모두 등록했습니다. 메서드 필터가 응답을 완료한 뒤 나머지 두 필터도 실행되나요? 핸들러가 직접 오류를 잡고 'fallback'을 돌려주는 경우와도 비교하세요.

해설 보기

메서드 필터가 처리하면 그 예외를 나머지 필터가 이어서 처리하지 않습니다. 가장 가까운 필터 하나만 실행됩니다.

핸들러가 오류를 직접 잡아 정상 값을 돌려준 경우에는 처리되지 않은 예외가 없으므로, 어떤 예외 필터도 실행되지 않고 'fallback'이 정상 응답으로 나갑니다.

5. 메모리 중복 검사의 한계

UsersService는 처음에 'testuser'만 보관합니다. 차례로 'alice', 'alice', 'Alice'를 생성하면 각각 어떤 결과가 나오나요? 다른 필터는 없고 중복 검사 코드는 그대로입니다. 이 검사로 여러 서버가 함께 쓰는 DB의 중복까지 막을 수 있나요?

해설 보기

첫 'alice'는 배열에 추가되어 성공하고, 두 번째는 UserAlreadyExistsException으로 기본 409가 됩니다. 'Alice'는 대소문자가 다른 문자열이므로 지금의 코드에서는 성공합니다.

이 결과는 한 서비스 인스턴스의 메모리 상태와 대소문자를 구분하는 비교 규칙에서 나온 것입니다. DB를 함께 쓰는 여러 서버에서의 중복 생성은 무결성 제약 조건에서 다루는 DB의 고유 제약 조건(UNIQUE)과 충돌 처리처럼 저장소 쪽에서 보장해야 합니다.

다음 글 커스텀 데코레이터 만들기에서는 반복되는 요청 값 읽기와 메타데이터 지정을 사용자 정의 데코레이터로 묶는 방법을 다룹니다.