๐Ÿ’ฅ 07. ์—๋Ÿฌ ์ฒ˜๋ฆฌ โ€” ๊ณ ๊ฐ์—๊ฒŒ ์นœ์ ˆํ•œ '๋ถˆ๋งŒ ์ฒ˜๋ฆฌ ์ „๋‹ด๋ฐ˜' ๋งŒ๋“ค๊ธฐ

2026๋…„ 2์›” 26์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

์„œ๋ฒ„๊ฐ€ ๋ป—์ง€ ์•Š๋„๋ก ์˜ˆ๋ฐฉํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ํ•ญ์ƒ ์นœ์ ˆํ•˜๊ณ  ์ผ๊ด€๋œ ์—๋Ÿฌ ์‘๋‹ต์„ ๋‚ด๋ ค์ฃผ๋Š” ์ „์—ญ ์˜ˆ์™ธ ํ•„ํ„ฐ(Exception Filter)๋ฅผ ๋งˆ์Šคํ„ฐํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„

๐Ÿงณ ์ „์ œ ์ง€์‹ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • HTTP ์ƒํƒœ ์ฝ”๋“œ(400, 401, 404, 500 ๋“ฑ)์˜ ๋Œ€๋žต์ ์ธ ์˜๋ฏธ๋ฅผ ์•Œ๊ณ  ์žˆ๋‹ค.
  • JavaScript์˜ try-catch ๋ฌธ์˜ ์—ญํ• ๊ณผ throw new Error() ๋ฌธ๋ฒ•์„ ์ดํ•ดํ•œ๋‹ค.

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
NestJS ๋‚ด์žฅ ์—๋Ÿฌ ๋ฑ‰๊ธฐ โ†’ ๋‚˜๋งŒ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ์—๋Ÿฌ๋กœ ํฌ์žฅํ•˜๊ธฐ โ†’ ์„œ๋ฒ„ ํญ๋ฐœ์„ ๋ง‰๋Š” ์ „์—ญ ๊ทธ๋ฌผ๋ง(Filter) ๋˜์ง€๊ธฐ โ†’ ์‹ค๋ฌด ๋กœ๊น… ๋ฐ ์—๋Ÿฌ ์ฝ”๋“œ ๊ด€๋ฆฌ๋ฒ•

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ง€์ €๋ถ„ํ•œ try-catch ๋„๋ฐฐ๋ฅผ ๊ฑท์–ด๋‚ด๊ณ  ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ 2๋ฐฐ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค.
  • ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ํŒŒ์‹ฑํ•˜๊ธฐ ์‰ฌ์šด "์ผ๊ด€๋œ JSON ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท"์„ ๊ทœ๊ฒฉํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์•Œ ์ˆ˜ ์—†๋Š” ์น˜๋ช…์  500 ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ง€์ •๋œ ์ฑ„๋„(Slack ๋“ฑ)๋กœ ์ฆ‰์‹œ ์•Œ๋ฆผ์„ ์˜๋Š” ๊ตฌ์กฐ๋ฅผ ์งค ์ˆ˜ ์žˆ๋‹ค.

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

์ดˆ๋ณด ์‹œ์ ˆ ๋ฐฑ์—”๋“œ ์ฝ”๋“œ์˜ ๊ฐ€์žฅ ํฐ ํŠน์ง•์€ ๋ผ์šฐํ„ฐ๋งˆ๋‹ค try { ... } catch (e) { res.status(500).send(e) } ๊ฐ€ ๋„๋ฐฐ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ.
์ด๋ ‡๊ฒŒ ์งœ๋ฉด ํ”„๋ก ํŠธ์—”๋“œ ์ž…์žฅ์—์„œ๋Š” ์–ด๋–ค API๋Š” ์—๋Ÿฌ๋ฅผ ํ…์ŠคํŠธ๋กœ ์ฃผ๊ณ , ์–ด๋–ค API๋Š” JSON์œผ๋กœ ์ฃผ๊ณ , ์‘๋‹ต ํ˜•ํƒœ๊ฐ€ ์ œ๋ฉ‹๋Œ€๋กœ๋ผ ์—๋Ÿฌ ํŒ์—…์„ ๋„์šฐ๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ๊ณ ํ†ต์ด ๋ผ.
NestJS์˜ ์˜ˆ์™ธ ํ•„ํ„ฐ(Exception Filter) ๋ฅผ ๋ฐฐ์šฐ๋ฉด, ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ ๊ณณ์œผ๋กœ ์‹น ๋ชฐ์•„์„œ(์ค‘์•™ ์ง‘์ค‘ํ™”) ์šฐ์•„ํ•˜๊ฒŒ ํ”„๋ก ํŠธ์—”๋“œ์—๊ฒŒ ์ผ๊ด€๋œ ์–‘์‹์„ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์žˆ์–ด.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

๋‚˜์œ ์‹๋‹น (์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋‹ค ์ฒ˜๋ฆฌํ•จ):
์ฃผ๋ฐฉ์žฅ(Service)์ด ์š”๋ฆฌ๋ฅผ ํƒœ์› ์–ด. ํ™€์„œ๋น™ ์ง์›(Controller)์ด ์ฃผ๋ฐฉ์— ๋“ค์–ด๊ฐ€์„œ "์•„์ด๊ณ  ๋งํ–ˆ๋„ค!" ํ•˜๊ณ  ์ง์ ‘ ์†๋‹˜ํ•œํ…Œ ๋‹ฌ๋ ค๊ฐ€ "์š”๋ฆฌ๊ฐ€ ํƒ€์„œ ๋ชป ๋จน์Šต๋‹ˆ๋‹ค, 500์› ๋Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”"๋ผ๊ณ  ์‚ฌ๊ณผํ•ด. ์ง์›์ด 100๋ช…์ด๋ฉด ์‚ฌ๊ณผ๋ฒ•๋„ 100๊ฐ€์ง€์•ผ.
์ข‹์€ ์‹๋‹น (์˜ˆ์™ธ ํ•„ํ„ฐ ์‚ฌ์šฉ):
์ฃผ๋ฐฉ์žฅ์€ ๊ทธ๋ƒฅ ์š”๋ฆฌ๋ฅผ ํƒœ์šฐ๋ฉด ์ฃผ๋ฐฉ ๋ฐ”๋‹ฅ์— ๋ถˆ๋Ÿ‰ ์Šคํ‹ฐ์ปค(Exception)๋ฅผ ํƒ ๋˜์ง€๊ณ  ์ฟจํ•˜๊ฒŒ ๋Œ์•„์„œ ๋ฒ„๋ ค!
ํ™€์„œ๋น™ ์ง์›๋„ ์‹ ๊ฒฝ ์•ˆ ์จ. ๋Œ€์‹  ์‹๋‹น์—๋Š” '๊ณ ๊ฐ ๋ถˆ๋งŒ ์ฒ˜๋ฆฌ ์ „๋‹ด๋ฐ˜(Exception Filter)' ์ด๋ผ๋Š” ๋ถ€์„œ๊ฐ€ ์ˆจ์–ด์žˆ์ง€. ์ฃผ๋ฐฉ์—์„œ ๋ˆ„๊ฐ€ ๋ถˆ๋Ÿ‰ ์Šคํ‹ฐ์ปค๋ฅผ ๋˜์ง€๋Š” ์ˆœ๊ฐ„ 0.1์ดˆ ๋งŒ์— ์ „๋‹ดํŒ€์ด ์ถœ๋™ํ•ด์„œ ๋‚š์•„์ฑˆ ๋‹ค์Œ, "๊ณ ๊ฐ๋‹˜, ๋ฉ”๋‰ด๊ฐ€ ํ’ˆ์ ˆ์ž…๋‹ˆ๋‹ค(๊น”๋”ํ•œ JSON ํ˜•ํƒœ)" ๋ผ๋ฉฐ ๋งค์šฐ ์นœ์ ˆํ•˜๊ณ  ํ†ต์ผ๋œ ํ†ค์œผ๋กœ ์‘๋Œ€ํ•˜๋Š” ๊ฑฐ์•ผ.


๐Ÿงฉ ๋‚ด์žฅ ์˜ˆ์™ธ์™€ ์ปค์Šคํ…€ ์˜ˆ์™ธ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์ƒํ™ฉ์— ๋งž๋Š” HTTP ์˜ˆ์™ธ ํด๋ž˜์Šค๋ฅผ ๋˜์งˆ(throw) ์ค„ ์•ˆ๋‹ค.

NestJS๊ฐ€ ์ค€๋น„ํ•ด ๋‘” ๊ธฐ๋ณธ ์˜ˆ์™ธ๋“ค (๋ฌด๊ธฐ๊ณ )

NestJS๋Š” HttpException์„ ์ƒ์†๋ฐ›์€ ์ˆ˜๋งŽ์€ ์ƒํƒœ์ฝ”๋“œ๋ณ„ ๋ฌด๊ธฐ๋“ค์„ ์ด๋ฏธ ๋‹ค ์ค€๋น„ํ•ด๋†จ์–ด.

import { NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
 
@Injectable()
export class UsersService {
  async getProfile(id: string) {
    const user = await this.db.findById(id);
    
    // ์œ ์ €๊ฐ€ ์—†๋‹ค๋ฉด ํ•„ํ„ฐ์—๊ฒŒ ํญํƒ„(์˜ˆ์™ธ)์„ ๋˜์ง!
    if (!user) {
      // 404 ์ƒํƒœ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‹ด๊ธด ์˜ˆ์™ธ ๋ฐœ์ƒ
      throw new NotFoundException(`ํšŒ์›๋ฒˆํ˜ธ ${id}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); 
    }
    return user;
  }
}

[์ฃผ์š” ๋‚ด์žฅ ์˜ˆ์™ธ ๋ชฉ๋ก]

  • BadRequestException (400) - ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ๋ฐ”๋””๊ฐ’์ด ํ‹€๋ ธ์Œ (04์žฅ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํŒŒ์ดํ”„๊ฐ€ ๋ฑ‰๋Š” ๊ฒƒ๋„ ์ด๊ฑฐ์•ผ)
  • UnauthorizedException (401) - ํ† ํฐ ์—†์Œ/ํ‹€๋ฆผ (06์žฅ ๊ฐ€๋“œ๊ฐ€ ๋ฑ‰์Œ)
  • NotFoundException (404) - ๋ฆฌ์†Œ์Šค๋ฅผ ๋ชป ์ฐพ์Œ
  • ConflictException (409) - ์ด๋ฉ”์ผ ์ค‘๋ณต๊ฐ€์ž…, ์ด๋ฏธ ์ข‹์•„์š” ๋ˆ„๋ฆ„ ๋“ฑ ๋กœ์ง ์ถฉ๋Œ
  • InternalServerErrorException (500) - DB๊ฐ€ ์ฃฝ์—ˆ๊ฑฐ๋‚˜ ๋‚ด ์ฝ”๋“œ์— ๋ฒ„๊ทธ๊ฐ€ ์žˆ๊ฑฐ๋‚˜

๋‚˜๋งŒ์˜ ์ปค์Šคํ…€ ์˜ˆ์™ธ ๋งŒ๋“ค๊ธฐ

"์ž”์•ก ๋ถ€์กฑ" ๊ฐ™์€ ๋น„์ฆˆ๋‹ˆ์Šค ํŠนํ™” ์—๋Ÿฌ๋Š” 400 ์—๋Ÿฌ์ง€๋งŒ, ํ‰๋ฒ”ํ•œ 400 ์—๋Ÿฌ์™€๋Š” ๊ตฌ๋ถ„ ์ง€์–ด์ฃผ๊ณ  ์‹ถ์–ด!

// common/exceptions/business.exception.ts
export class InsufficientBalanceException extends BadRequestException {
  constructor(required: number, current: number) {
    // ํ”„๋ก ํŠธ์—”๋“œ์—๊ฒŒ ์ค„ JSON ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ธํ•˜๊ฒŒ ๋งˆ์Œ๋Œ€๋กœ ์กฐ๋ฆฝ!
    super({
      errorCode: 'INSUFFICIENT_BALANCE',
      message: '๋ˆ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค ใ… ใ… ',
      details: { required, current } // ๋‚จ์€ ์ž”์•ก์„ ์•Œ๋ ค์คŒ
    });
  }
}

๐Ÿงฉ ์ „์—ญ ์˜ˆ์™ธ ํ•„ํ„ฐ (Global Exception Filter) ๐ŸŸข

์ด์ œ ์ฃผ๋ฐฉ(์„œ๋น„์Šค/์ปจํŠธ๋กค๋Ÿฌ)์—์„œ ๋˜์ง€๋Š” ๋ชจ๋“  ์—๋Ÿฌ๋ฅผ ํ—ˆ๊ณต์—์„œ ๋‚š์•„์ฑ„์„œ, ์˜ˆ์œ ํฌ์žฅ์ง€์— ๋‹ด์•„์ฃผ๋Š” '๋ถˆ๋งŒ ์ฒ˜๋ฆฌ ์ „๋‹ด๋ฐ˜'์„ ๋งŒ๋“ค ์ฐจ๋ก€์•ผ.

// common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express'; // Fastify ์“ฐ๋ฉด FastifyReply ์ ์šฉ
 
@Catch() // ๐Ÿ‘ˆ ์•„๋ฌด ๊ด„ํ˜ธ๋„ ์•ˆ ์ ์œผ๋ฉด '์—๋Ÿฌ๋‚˜๋Š” ๋ชจ๋“  ๊ฒƒ'์„ ๋‹ค ์žก์•„์ฑ„๊ฒ ๋‹ค๋Š” ๋œป!
export class GlobalExceptionFilter implements ExceptionFilter {
  
  // ์—๋Ÿฌ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜๋Š” ์ˆœ๊ฐ„ ์—ฌ๊ธฐ๊ฐ€ ๋ฌด์กฐ๊ฑด ์‹คํ–‰๋จ
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    // 1. ์ƒํƒœ ์ฝ”๋“œ ํŒ๋ณ„ (๋‚ด๊ฐ€ ๋˜์ง„ ์—๋Ÿฌ๋ฉด ๊ทธ ์ฝ”๋“œ, ์•„๋‹ˆ๋ฉด ๋ฌด์กฐ๊ฑด 500 ์ฒ˜๋ฆฌ)
    const status = 
      exception instanceof HttpException 
        ? exception.getStatus() 
        : HttpStatus.INTERNAL_SERVER_ERROR;
 
    // 2. ์ตœ์ข… ํ”„๋ก ํŠธ์—”๋“œ์—๊ฒŒ ๋‚ด๋ ค๊ฐˆ ํ†ต์ผ๋œ JSON ํฌ๋งท!
    response.status(status).json({
      success: false,   // ๋ฌด์กฐ๊ฑด ๋“ค์–ด๊ฐ€๋Š” ๊ณตํ†ต ์ƒํƒœ
      timestamp: new Date().toISOString(),
      path: request.url,
      
      // ๋‚ด๊ฐ€ ๋˜์ง„ ์—๋Ÿฌ๋ผ๋ฉด ์•Œ๋งน์ด๋ฅผ ๋นผ์˜ค๊ณ , ์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๋ฉด ์ž„์˜ ๋ฉ”์„ธ์ง€
      error: exception instanceof HttpException 
        ? exception.getResponse() 
        : '์•—! ์„œ๋ฒ„ ๋‚ด๋ถ€ ๋„คํŠธ์›Œํฌ๋ง ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค!',
    });
  }
}

์ด ํ•„ํ„ฐ๋ฅผ ๊ฑด๋ฌผ ์ „์ฒด์— ํ™œ์„ฑํ™”์‹œํ‚ค๋ฉด ๋์ด์•ผ. main.ts ๋กœ ๊ฐ„๋‹ค!

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // ๊ธ€๋กœ๋ฒŒ ํ•„ํ„ฐ ์žฅ์ฐฉ! ์ด์ œ 365์ผ ์—๋Ÿฌ ์•ˆ ๋†“์นจ.
  app.useGlobalFilters(new GlobalExceptionFilter());
  
  await app.listen(3000);
}

๐Ÿงช ๋”ฐ๋ผํ•ด๋ณด๊ธฐ: ๊ถ๊ทน์˜ ๊ธ€๋กœ๋ฒŒ ํ•„ํ„ฐ ๊นŽ์•„๋ณด๊ธฐ

์‹ค๋ฌด์—์„œ๋Š” NestJS ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ, Zod ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฒ€์‚ฌ ์—๋Ÿฌ, ์•Œ ์ˆ˜ ์—†๋Š” ์น˜๋ช…์  500 ์—๋Ÿฌ ๋“ฑ์„ ํ•„ํ„ฐ ์•ˆ์—์„œ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด. ๊ฑฐ๊ธฐ์— 500 ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ„ฐ๋ฏธ๋„ ๋กœ๊น…๊นŒ์ง€ ๋ถ™์—ฌ๋ณด์ž.

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { ZodError } from 'zod'; // Zod ์“ธ ๋•Œ
 
@Catch()
export class MegaGlobalFilter implements ExceptionFilter {
  // ๋กœ๊ฑฐ ์žฅ์ฐฉ
  private readonly logger = new Logger(MegaGlobalFilter.name);
 
  // HttpAdapterHost๋ฅผ ์“ฐ๋ฉด Express๋“  Fastify๋“  ํ”„๋ ˆ์ž„์›Œํฌ ์ข…์†์„ฑ ์—†์ด ์‘๋‹ต(Reply) ๊ฐ€๋Šฅ!
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
 
  catch(exception: unknown, host: ArgumentsHost) {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
 
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = '์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
    let errorCode = 'UNKNOWN_ERROR';
 
    // ๋ถ„๊ธฐ 1: ๋‚ด๊ฐ€ ์•Œ๊ณ  ๋˜์ง„ HTTP ์—๋Ÿฌ (400, 401, 404 ๋“ฑ)
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const res = exception.getResponse() as any;
      message = typeof res === 'string' ? res : (res.message || 'Error');
      errorCode = res.errorCode || 'HTTP_ERROR';
    } 
    // ๋ถ„๊ธฐ 2: Zod ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ์‹คํŒจ ์—๋Ÿฌ
    else if (exception instanceof ZodError) {
      status = HttpStatus.BAD_REQUEST;
      message = '์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.';
      errorCode = 'ZOD_VALIDATION_ERROR';
    } 
    // ๋ถ„๊ธฐ 3: ์ง„์งœ ์„œ๋ฒ„๊ฐ€ ํ„ฐ์ง€๋Š” ๋ฒ„๊ทธ (null.abc ์ ‘๊ทผ, DB ์ ‘์† ์ง€์—ฐ ๋“ฑ) => 500
    else {
      // ๐Ÿšจ ์น˜๋ช…์  ์—๋Ÿฌ๋Š” ๋ฌด์กฐ๊ฑด ์ž์„ธํ•˜๊ฒŒ ๋กœ๊น…! (๋˜๋Š” ์—ฌ๊ธฐ์„œ ์Šฌ๋ž™ API ํ˜ธ์ถœ)
      this.logger.error(
        `[${request.method}] ${request.url} - ${exception}`,
        (exception as Error)?.stack,
      );
    }
 
    // ํ†ต์ผ๋œ ์‘๋‹ต ๊ทœ๊ฒฉ
    const responseBody = {
      statusCode: status,
      errorCode,
      message,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(request),
    };
 
    httpAdapter.reply(ctx.getResponse(), responseBody, status);
  }
}

์•ฑ์— ๋“ฑ๋กํ•  ๋•Œ๋Š” HttpAdapterHost ์ฃผ์ž…์— ์น˜์‚ฌํ•˜๊ฒŒ ์ฃผ์˜ํ•ด์•ผ ํ•ด:

// main.ts
const httpAdapterHost = app.get(HttpAdapterHost); // ์–ด๋Œ‘ํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ!
app.useGlobalFilters(new MegaGlobalFilter(httpAdapterHost));

๐Ÿ’ผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์™€ ์‹ค๋ฌด ํŒ ๐ŸŸก

1. ์ปจํŠธ๋กค๋Ÿฌ ์•ˆ์—์„œ ๋ช…์‹œ์ ์ธ try-catch๋Š” ์ตœ๋Œ€ํ•œ ์ง€์šด๋‹ค.

๊ฐ€์žฅ ๋งŽ์ด ํ•˜๋Š” ์ดˆ๋ณด์˜ ์‹ค์ˆ˜๊ฐ€ ๋ชจ๋“  ๋ผ์šฐํ„ฐ๋งˆ๋‹ค try-catch๋ฅผ ๋ง์”Œ์šฐ๋Š” ๊ฑฐ์•ผ.
"๋งŒ์•ฝ ๋กœ์ง ์งœ๋‹ค ์—๋Ÿฌ ๋‚˜๋ฉด ์–ด๋–กํ•ด์š”?" -> ์–ด์ฐจํ”ผ ๊ธ€๋กœ๋ฒŒ ํ•„ํ„ฐ๊ฐ€ ๋‹ค ์žก์•„! ๊ทธ๋ƒฅ ์„œ๋น„์Šค ํ•จ์ˆ˜ ์ญ‰ ์“ฐ๊ณ  ๋งŒ์•ฝ ์ด์ƒํ•˜๋ฉด ์„œ๋น„์Šค ์•ˆ์—์„œ throw new NotFoundException() ๋งŒ ๋˜์ ธ ๋ฒ„๋ ค. ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์„ธ์ƒ ํŽธํ•˜๊ฒŒ ๋กœ์ง๋งŒ ํ˜ธ์ถœํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ์™„๋ฒฝํ•œ ํ˜•ํƒœ์•ผ.

2. ์—๋Ÿฌ ์ฝ”๋“œ๋Š” ํ•œ ๊ณณ์— ๋ชจ์•„ ์ƒ์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๋ผ

ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์—๋Ÿฌ ์ฝ”๋“œ๋ฅผ ๋งˆ๊ตฌ ์“ฐ๋ฉด, ๋‚˜์ค‘์— ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•  ๋•Œ ๊ณ ํ†ต์Šค๋Ÿฌ์›Œ.

// common/error-codes.ts
export const ErrorCodes = {
  USER_NOT_FOUND: 'USER_NOT_FOUND',
  EMAIL_EXISTS: 'EMAIL_EXISTS',
  TOKEN_EXPIRED: 'TOKEN_EXPIRED',
} as const;
 
// ์‚ฌ์šฉํ•  ๋•Œ
throw new BadRequestException({ errorCode: ErrorCodes.EMAIL_EXISTS });

๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด.

โŒ UnhandledPromiseRejectionWarning (์„œ๋ฒ„ ๋‹ค์šด)

ํ˜„์ƒ:
์„œ๋ฒ„๊ฐ€ ์™„์ „ํžˆ ํ‘น ๊บผ์ง. ๋ฌด์–ธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ๋ผ๋ฉฐ ๋ป˜๊ฑด ๊ธ€์”จ ์žฅ๋ฌธ์ด ํ„ฐ๋ฏธ๋„์— ๋œธ.
์›์ธ:
๋†€๋ž๊ฒŒ๋„ ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ ํ•„ํ„ฐ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์€ ์—๋Ÿฌ์•ผ. ์ด ๊ฒฝ์šฐ๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜ (Promise) ์•ˆ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ๋Š”๋ฐ await๋ฅผ ์•ˆ ๊ฑธ์–ด์ค˜์„œ ํƒ€์ด๋ฐ์ด ์–ด๊ธ‹๋‚œ ์ฑ„ ์šฐ์ฃผ ๋ฐ– ๋ฏธ์•„๋กœ ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ ธ๋ฒ„๋ฆฐ ๊ฒฝ์šฐ์•ผ.
ํ•ด๊ฒฐ์ฑ…:
์—๋Ÿฌ๊ฐ€ ๋‚œ Promise๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ํ•จ์ˆ˜(์˜ˆ: DB ์ €์žฅ ๋กœ์ง) ์•ž์— await๋ฅผ ์ž˜ ๊ธฐ์žฌํ–ˆ๋Š”์ง€ ์ฒดํฌ!

โŒ TypeError: Cannot read properties of undefined (reading 'switchToHttp')

์›์ธ:
ํ•„ํ„ฐ์—์„œ ArgumentsHost๋ฅผ ์จ์•ผ ํ•˜๋Š”๋ฐ ๋ญ”๊ฐ€ ๊ฐ์ฒด ๊ตฌ์กฐ ์ธ์‹์„ ์ž˜๋ชปํ–ˆ์Œ. ์ฃผ๋กœ WebSocket์ด๋‚˜ GraphQL ๊ฐ™์ด HTTP๊ฐ€ ์•„๋‹Œ ํ™˜๊ฒฝ์—์„œ ๊ฐ•์ œ๋กœ .switchToHttp()๋ฅผ ์–ต์ง€๋กœ ๊บผ๋‚ด๋ ค ํ•  ๋•Œ ๋ฐœ์ƒํ•˜๊ธฐ๋„ ํ•ด.
ํ•ด๊ฒฐ์ฑ…:
๋งŒ์•ฝ HTTP ์ „์šฉ ์•ฑ์ด ์•„๋‹ˆ๋ผ๋ฉด host.getType() === 'http' ๋กœ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค˜์•ผ ์•ˆ์ „ํ•ด.


๐Ÿ—‚๏ธ ์น˜ํŠธ์‹œํŠธ โ€” ์‹ค๋ฌด ์š”์•ฝ ์นด๋“œ

์ƒํ™ฉ์‚ฌ์šฉ ์˜ˆ์‹œ๋น„๊ณ 
๋ฆฌ์†Œ์Šค ๋ชป ์ฐพ์Œthrow new NotFoundException()HTTP 404 ๋‚ด๋ ค๊ฐ
๋น„์ฆˆ๋‹ˆ์Šค ์—๋Ÿฌthrow new BadRequestException()๋Œ€๋ถ€๋ถ„์˜ ์—๋Ÿฌ (400)
ํŒŒ์ดํ”„/๊ฐ€๋“œ ๋ฐ–์—์„œ ๋‹ค ์žก๊ธฐ@Catch() ๋‹ฌ๋ฆฐ ํด๋ž˜์Šค ์ƒ์„ฑ๊ธ€๋กœ๋ฒŒ ํ•„ํ„ฐ ์™„์„ฑ
์ „์—ญ ํ•„ํ„ฐ ์ ์šฉapp.useGlobalFilters(new MyFilter())main.ts ํ•„์ˆ˜ ๊ธฐ์žฌ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. NestJS์—์„œ ์ข‹์€ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๊ถŒ์žฅํ•˜๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ค‘ ๊ฐ€์žฅ ์˜ณ์€ ๊ฒƒ์€?

  • A) ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋ผ์šฐํ„ฐ ๋‚ด๋ถ€์— try { } catch() { } ๋ธ”๋ก์„ ์ž‘์„ฑํ•˜์—ฌ ์ง์ ‘ 500 ์ƒํƒœ ์ฝ”๋“œ๋ฅผ res.send() ํ•œ๋‹ค.
  • B) ์„œ๋น„์Šค(Service) ๋ ˆ์ด์–ด์˜ ๋ชจ๋“  ๋ฐ˜ํ™˜๊ฐ’์„ ์—๋Ÿฌ์ธ์ง€ ํ™•์ธํ•˜๋Š” if (result.error) ๊ฒ€์‚ฌ๋ฅผ ์ด˜์ด˜ํžˆ ์ง ๋‹ค.
  • C) ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ณ ๋ฏผ์„ ๋œ๊ณ , ๋กœ์ง ์ค‘ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ์˜ˆ์™ธ(Exception)๋ฅผ throwํ•˜์—ฌ ์ „์—ญ ํ•„ํ„ฐ๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋งก๊ธด๋‹ค.

โœ… ์ •๋‹ต: C

๐Ÿ’ก ์„ค๋ช…: ํ•„ํ„ฐ์˜ ์กด์žฌ ์ด์œ ์•ผ. ํ™€์„œ๋น™ ์ง์›(Controller)์€ ์š”๋ฆฌ ์ฃผ๋ฌธ๋งŒ ๋ฐ›๊ณ  ๊ฐ–๋‹ค์ฃผ๊ณ  ํŽธํ•˜๊ฒŒ ์ผํ•ด์•ผ ํ•ด. ๋Œ๋ฐœ ์ƒํ™ฉ ์ฒ˜๋ฆฌ๋Š” ๋ถˆ๋งŒ ์ฒ˜๋ฆฌ๋ฐ˜(Global Exception Filter)์— ์ค‘์•™์ง‘๊ถŒํ™” ์‹œ์ผœ์„œ ํ†ต์ผ๋˜๊ฒŒ ์‘๋‹ตํ•˜๋Š” ๊ฒƒ์ด ์ตœ๊ณ ์•ผ.

Q2. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ API ์‘๋‹ต์„ ๋ฐ›์•˜๋Š”๋ฐ ์—๋Ÿฌ ํ˜•ํƒœ๊ฐ€ ํŒŒ์‹ฑํ•˜๊ธฐ ๋„ˆ๋ฌด ํž˜๋“ค๋‹ค๊ณ  ํ•ญ์˜ํ•œ๋‹ค. ์ด๋ฅผ ์™„์ „ํžˆ ๊ทœ๊ฒฉํ™”(์˜ˆ: { success: false, msg: "...", code: "..." }) ํ•ด์„œ ํ•œ ๋ฐฉ์— ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์€๋ฐ, ๊ผญ ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ํด๋ž˜์Šค์˜ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์™€ ์ƒ์† ํƒ€์ž…์€ ๋ฌด์—‡์ธ๊ฐ€?

โœ… ์ •๋‹ต: @Catch() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋ถ™์ด๊ณ , ExceptionFilter ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„(implements)ํ•œ ํ•„ํ„ฐ ๊ฐ์ฒด๋ฅผ ์ž‘์„ฑํ•œ๋‹ค!

Q3. ๋””๋ฒ„๊น… ํ€ด์ฆˆ: ์•„๋ž˜ ์ฝ”๋“œ์—์„œ HttpAdapterHost๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ๋นผ๋จน์–ด์„  ์•ˆ ๋  main.ts ๋“ฑ๋ก ๊ณผ์ •์„ ์„œ์ˆ ํ•ด๋ด.

@Catch()
export class HttpFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
}

โœ… ์ •๋‹ต: ๋นŒ๋“œ๋œ app ๊ฐ์ฒด์—์„œ ์–ด๋Œ‘ํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ์ธ์ž๋กœ ๋„ฃ์–ด์ค˜์•ผ ํ•œ๋‹ค.
์„ค๋ช…: const httpAdapterHost = app.get(HttpAdapterHost); ์ด๋ ‡๊ฒŒ app.get() ๋ช…๋ น์–ด๋กœ ์ปจํ…Œ์ด๋„ˆ์—์„œ ๊บผ๋‚ด์˜จ ๋’ค์— app.useGlobalFilters(new HttpFilter(httpAdapterHost)) ์™€ ๊ฐ™์ด ๋„ฃ์–ด์ค˜์•ผ ์˜์กด์„ฑ์ด ๊นจ์ง€์ง€ ์•Š์•„.


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ