Blog

[NestJS] 9. 로깅과 AOP

Category
Author
citeFred
citeFred
PinOnMain
1 more property
NestJS, TypeORM 이해하기
Table of Content

1. Log란?

Log란 시스템이나 애플리케이션이 작동하면서 발생하는 다양한 사건이나 상태 변화를 기록한 정보
로그는 주로 텍스트 파일 또는 데이터베이스에 저장
시스템이 어떻게 동작하고 있는지, 어떤 오류가 발생했는지, 또는 사용자가 어떤 작업을 수행했는지 추적 하는 목적
로그의 목적
투명성: 로그는 시스템의 내부 동작을 투명하게 드러내며, 문제 발생 시 그 원인을 파악하는 데 중요한 자료를 제공
문제 해결 및 디버깅: 로그를 통해 개발자나 시스템 관리자들은 문제 발생 시 그 원인을 신속히 찾고 해결
보안: 보안 관련 로그는 시스템이 해킹 당했거나 보안 위협이 발생했을 때 이를 탐지하는 데 중요한 역할
감사와 규제 준수: 특히 금융, 의료 등 규제가 엄격한 산업에서는 로그를 통해 각종 규제 요구 사항을 준수했음을 증명
Logging Level
로그 메시지의 중요도나 심각도를 분류하는 개념
TRACE (가장 낮은 수준)
설명: 주로 애플리케이션의 매우 세부적인 실행 흐름을 추적하는 데 사용되며, 변수의 값이나 함수 호출의 세부 정보 등을 기록
사용 예시: 함수의 진입과 종료, 특정 루프의 각 반복에서 발생하는 상세한 처리 내용
사용 시기: 거의 모든 이벤트를 기록해야 하는 경우, 디버깅을 위해 매우 세밀한 정보를 필요로 할 때
DEBUG
설명: 개발 중 문제를 해결하기 위해 사용. 시스템의 내부 상태, 변수 값, 문제를 해결하는 데 필요한 진단 정보 등을 포함
사용 예시: 특정 조건에서만 발생하는 버그를 추적할 때, 코드의 흐름을 확인하고자 할 때
사용 시기: 개발 환경이나 테스트 환경에서 디버깅을 위해 사용
INFO
설명: 시스템의 정상적인 동작에 대한 일반 정보를 기록. 일반적인 실행 흐름을 설명하고, 시스템 상태나 주요 이벤트를 추적
사용 예시: 애플리케이션이 시작되었을 때, 중요한 사용자 활동(로그인, 로그아웃) 등이 발생했을 때
사용 시기: 시스템의 기본 운영 상태를 모니터링할 때, 정상적인 동작을 기록할 때
WARN
설명: 주의가 필요하지만, 즉각적인 문제가 발생하지는 않은 상황을 기록 잠재적인 문제나 향후 시스템 동작에 영향을 줄 수 있는 상황을 알리는 데 사용
사용 예시: 오래된 API가 사용되었을 때, 디스크 공간이 줄어들고 있는 상황 등
사용 시기: 잠재적인 문제를 감지하고, 관리자가 주의를 기울여야 할 때
ERROR
설명: 애플리케이션에서 오류가 발생했을 때 기록. 시스템이 정상적으로 동작하지 않으며, 문제를 해결하기 위한 조치가 필요
사용 예시: 데이터베이스 연결 실패, 예외 처리에서 발생한 오류 등
사용 시기: 시스템의 문제가 발생했을 때, 해당 문제를 추적하고 해결하기 위해 사용
FATAL (가장 높은 수준)
설명: 치명적인 오류로, 애플리케이션이 더 이상 실행될 수 없는 상황을 기록. 즉각적인 조치가 필요하며, 시스템이 강제로 종료될 수도 있음
사용 예시: 중요한 데이터 손실, 애플리케이션의 중대한 충돌 등.
사용 시기: 시스템의 즉각적인 복구가 필요한 경우, 심각한 문제로 인해 프로그램이 종료될 때.
로깅을 사용하는 주요 부분
1.
컨트롤러 (Controller):
역할: 주로 요청과 응답을 관리하고, 서비스 계층을 호출하여 비즈니스 로직을 처리
로깅 내용: 요청 수신, 응답 전송, 중요한 API 호출 및 사용자 활동 (예: 로그인, 데이터 수정) 등을 기록
2.
서비스 (Service):
역할: 애플리케이션의 비즈니스 로직을 처리하며, 데이터베이스 접근 또는 다른 서비스 호출 등을 수행
로깅 내용: 비즈니스 로직의 주요 단계, 데이터 처리 과정, 외부 API 호출 결과, 예외 발생 등을 기록. 특히, 복잡한 비즈니스 로직이나 중요한 트랜잭션의 경우 서비스 계층에서 로깅하는 것이 중요
3.
미들웨어 (Middleware):
역할: 요청이 컨트롤러에 도달하기 전에 처리해야 할 로직을 수행. 예를 들어, 요청 로깅, 인증, 또는 데이터 변환 등을 수행
로깅 내용: 모든 요청과 응답을 기록하여 전체 요청 흐름을 추적하거나, 특정 요청의 헤더, 본문 등을 기록
4.
가드 (Guard):
역할: 요청이 특정 조건을 만족하는지 확인하고, 요청을 허용할지 결정합니다. 예를 들어, 인증, 권한 검사 등을 처리
로깅 내용: 권한이 부족한 요청이 차단되었을 때, 또는 특정 사용자나 역할에 의해 접근이 시도되었을 때 이를 기록
5.
인터셉터 (Interceptor):
역할: 요청과 응답을 가로채어 추가적인 처리를 수행. 예를 들어, 응답 데이터의 변환, 성능 측정 등을 필요
로깅 내용: 요청 처리 시간 측정, 응답 변환 전후의 데이터, 요청의 메타데이터 등을 기록
6.
예외 필터 (Exception Filter):
역할: 애플리케이션에서 발생한 예외를 잡아 처리하고, 응답을 생성
로깅 내용: 예외 발생 시, 해당 예외의 상세 정보와 스택 트레이스, 발생한 컨텍스트 등을 기록

2. NestJS의 로깅

NestJS에 내장된 Logger 클래스는 여러 종류의 로깅 메서드를 제공
log(): 일반 정보 기록 (INFO 수준).
warn(): 주의가 필요한 상황에 대한 경고.
error(): 오류나 예외 발생 시 기록.
debug(): 디버깅을 위해 상세 정보 기록.
verbose(): 매우 상세한 정보나 프로세스 흐름 기록.
로거를 통해 로깅 사용해보기
main.ts
dotenv 환경변수로 PORT 변경
서버 실행 포트에 대한 로깅
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as cookieParser from 'cookie-parser' import { Logger } from '@nestjs/common'; import * as dotenv from 'dotenv'; dotenv.config(); async function bootstrap() { const app = await NestFactory.create(AppModule); // cookie parser 미들웨어 추가 app.use(cookieParser()); await app.listen(process.env.SERVER_PORT); Logger.log(`Application Running on Port : ${process.env.SERVER_PORT}`) } bootstrap();
TypeScript
복사
Service 계층 로깅 사용해보기
boards.service.ts
Service 계층에서 로거 인스턴스 생성 후 각 부분에 사용해보기
import { BadRequestException, Injectable, Logger, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { Board } from './boards.entity'; import { BoardStatus } from './boards-status.enum'; import { CreateBoardDto } from './dto/create-board.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { UpdateBoardDto } from './dto/update-board.dto'; import { User } from 'src/auth/users.entity'; @Injectable() export class BoardsService { private readonly logger = new Logger(BoardsService.name); // Logger 인스턴스 생성 // Repository 계층 DI constructor( @InjectRepository(Board) private boardRepository : Repository<Board> ){} // 게시글 조회 기능 async getAllBoards(): Promise<Board[]> { this.logger.verbose('Retrieving all boards'); const foundBoards = await this.boardRepository.find(); this.logger.verbose(`Retrieving all boards list Successfully`); return foundBoards; } // 로그인된 유저가 작성한 게시글 조회 기능 async getMyAllBoards(logginedUser: User): Promise<Board[]> { this.logger.verbose(`User ${logginedUser.username} is retrieving their own boards`); // 기본 조회에서는 엔터티를 즉시로딩으로 변경해야 User에 접근 할 수 있다. // const foundBoards = await this.boardRepository.findBy({ user: logginedUser }); // 쿼리 빌더를 통해 lazy loading 설정된 엔터티와 관계를 가진 엔터티(User) 명시적 접근이 가능하다. const foundBoards = await this.boardRepository.createQueryBuilder('board') .leftJoinAndSelect('board.user', 'user') // 사용자 정보를 조인(레이지 로딩 상태에서 User 추가 쿼리) .where('board.userId = :userId', { userId : logginedUser.id }) .getMany(); this.logger.verbose(`Retrieving ${logginedUser.username}'s boards list Successfully`); return foundBoards; } // 특정 게시글 조회 기능 async getBoardDetailById(id: number): Promise<Board> { this.logger.verbose(`Retrieving board with ID ${id}`); const foundBoard = await this.boardRepository.createQueryBuilder('board') .leftJoinAndSelect('board.user', 'user') // 사용자 정보를 조인 .where('board.id = :id', { id }) .getOne(); if (!foundBoard) { throw new NotFoundException(`Board with ID ${id} not found`); } this.logger.verbose(`Retrieving board by id:${id} Successfully`); return foundBoard; } // 키워드(작성자)로 검색한 게시글 조회 기능 async getBoardsByKeyword(author: string): Promise<Board[]> { this.logger.verbose(`Retrieving boards by author: ${author}`); if (!author) { throw new BadRequestException('Author keyword must be provided'); } const foundBoards = await this.boardRepository.findBy({ author: author }) if (foundBoards.length === 0) { throw new NotFoundException(`No boards found for author: ${author}`); } this.logger.verbose(`Retrieving boards by author: ${author} Successfully`); return foundBoards; } // 게시글 작성 기능 async createBoard(createBoardDto: CreateBoardDto, logginedUser: User): Promise<Board> { this.logger.verbose(`User ${logginedUser.username} is creating a new board with title: ${createBoardDto.title}`); const { title, contents } = createBoardDto; if (!title || !contents) { throw new BadRequestException('Title, and contents must be provided'); } const newBoard = this.boardRepository.create({ author: logginedUser.username, title, contents, status: BoardStatus.PUBLIC, user: logginedUser }); const createdBoard = await this.boardRepository.save(newBoard); this.logger.verbose(`Board created by User ${logginedUser.username}`); return createdBoard; } // 특정 번호의 게시글 수정 async updateBoardById(id: number, updateBoardDto: UpdateBoardDto): Promise<Board> { this.logger.verbose(`Attempting to update board with ID ${id}`); const foundBoard = await this.getBoardDetailById(id); const { title, contents } = updateBoardDto; if (!title || !contents) { throw new BadRequestException('Title and contents must be provided'); } foundBoard.title = title; foundBoard.contents = contents; const updatedBoard = await this.boardRepository.save(foundBoard) this.logger.verbose(`Board with ID ${id} updated successfully`); return updatedBoard; } // 특정 번호의 게시글 일부 수정 async updateBoardStatusById(id: number, status: BoardStatus): Promise<void> { this.logger.verbose(`ADMIN is attempting to update the status of board with ID ${id} to ${status}`); const result = await this.boardRepository.update(id, { status }); if (result.affected === 0) { throw new NotFoundException(`Board with ID ${id} not found`); } this.logger.verbose(`Board with ID ${id} status updated to ${status} by Admin`); } // 게시글 삭제 기능 async deleteBoardById(id: number, logginedUser: User): Promise<void> { this.logger.verbose(`User ${logginedUser.username} is attempting to delete board with ID ${id}`); const foundBoard = await this.getBoardDetailById(id); // 작성자와 요청한 사용자가 같은지 확인 if (foundBoard.user.id !== logginedUser.id) { throw new UnauthorizedException('Do not have permission to delete this board') } await this.boardRepository.delete(foundBoard); this.logger.verbose(`Board with ID ${id} deleted by User ${logginedUser.username}`); } }
TypeScript
복사
auth.service.ts
import { ConflictException, Injectable, UnauthorizedException, Logger, Res } from '@nestjs/common'; import { Response, Request } from 'express'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './user.entity'; import { Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; import * as bcrypt from 'bcryptjs'; import { LoginUserDto } from './dto/login-user.dto'; import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( @InjectRepository(User) private usersRepository: Repository<User>, private jwtService: JwtService ){} // 회원 가입 async signUp(createUserDto: CreateUserDto): Promise<User> { const { username, password, email, role } = createUserDto; this.logger.verbose(`Attempting to sign up user with email: ${email}`); // 이메일 중복 확인 await this.checkEmailExists(email); // 비밀번호 해싱 const hashedPassword = await this.hashPassword(password); const user = this.usersRepository.create({ username, password: hashedPassword, // 해싱된 비밀번호 사용 email, role, }); return await this.usersRepository.save(user); } // 로그인 async signIn(loginUserDto: LoginUserDto, @Res() res: Response): Promise<void> { const { email, password } = loginUserDto; this.logger.verbose(`Attempting to sign in user with email: ${email}`); try { const existingUser = await this.findUserByEmail(email); if (!existingUser || !(await bcrypt.compare(password, existingUser.password))) { this.logger.warn(`Failed login attempt for email: ${email}`); throw new UnauthorizedException('Incorrect email or password.'); } // [1] JWT 토큰 생성 (Secret + Payload) const payload = { email: existingUser.email, username: existingUser.username, role: existingUser.role }; const accessToken = await this.jwtService.sign(payload); // [2] JWT를 쿠키에 저장 및 response에 쿠키 담기 res.cookie('Authorization', accessToken, { httpOnly: true, // 클라이언트 측 스크립트에서 쿠키 접근 금지 secure: false, // HTTPS에서만 쿠키 전송, 임시 비활성화 maxAge: 3600000, // 1시간 sameSite: 'none', // CSRF 공격 방어 }); this.logger.verbose(`User signed in successfully with email: ${email}`); res.send({ message: 'Logged in successfully' }); } catch (error) { this.logger.error('Signin failed', error.stack); throw error; } } // 이메일 중복 확인 메서드 private async checkEmailExists(email: string): Promise<void> { this.logger.verbose(`Checking if email exists: ${email}`); const existingUser = await this.findUserByEmail(email); if (existingUser) { this.logger.warn(`Email already exists: ${email}`); throw new ConflictException('Email already exists'); } this.logger.verbose(`Email is available: ${email}`); } // 이메일로 유저 찾기 메서드 private async findUserByEmail(email: string): Promise<User | undefined> { return await this.usersRepository.findOne({ where: { email } }); } // 비밀번호 해싱 암호화 메서드 private async hashPassword(password: string): Promise<string> { this.logger.verbose(`Hashing password`); const salt = await bcrypt.genSalt(); // 솔트 생성 return await bcrypt.hash(password, salt); // 비밀번호 해싱 } }
TypeScript
복사
Controller 계층 로깅 사용해보기
boards.controller.ts
Controller 계층에서 로거 인스턴스 생성 후 각 부분에 사용해보기
import { Body, Controller, Delete, Get, Logger, Param, Patch, Post, Put, Query, UseGuards } from '@nestjs/common'; import { BoardsService } from './boards.service'; import { Board } from './board.entity'; import { CreateBoardDto } from './dto/create-board.dto'; import { BoardStatus } from './board-status.enum'; import { UpdateBoardDto } from './dto/update-board.dto'; import { BoardStatusValidationPipe } from './pipes/board-status-validation.pipe'; import { AuthGuard } from '@nestjs/passport'; import { RolesGuard } from 'src/auth/custom-role.guard'; import { Roles } from 'src/auth/roles.decorator'; import { UserRole } from 'src/auth/user-role.enum'; import { GetUser } from 'src/auth/get-user.decorator'; import { User } from 'src/auth/user.entity'; @Controller('api/boards') @UseGuards(AuthGuard('jwt'), RolesGuard) // JWT 인증과 role 커스텀 가드를 적용 export class BoardsController { private readonly logger = new Logger(BoardsController.name); // Logger 인스턴스 생성 // 생성자 주입(DI) constructor(private boardsService: BoardsService){} // 게시글 작성 기능 @Post('/') // PostMapping 핸들러 데코레이터 @Roles(UserRole.USER) // User만 게시글 작성 가능 createBoard(@Body() createBoardDto: CreateBoardDto, @GetUser() user: User): Promise<Board> { this.logger.verbose(`User ${user.username} creating a new board. Data: ${JSON.stringify(createBoardDto)}`); return this.boardsService.createBoard(createBoardDto, user) } // 게시글 조회 기능 @Get('/') // GetMapping 핸들러 데코레이터 getAllBoards(): Promise<Board[]> { this.logger.verbose('Retrieving all boards'); return this.boardsService.getAllBoards(); } // 나의 게시글 조회 기능 @Get('/myboards') // GetMapping 핸들러 데코레이터 getMyAllBoards(@GetUser() user: User): Promise<Board[]> { this.logger.verbose(`User ${user.username} retrieving their boards`); return this.boardsService.getMyAllBoards(user); } // 특정 번호의 게시글 조회 @Get('/:id') getBoardById(@Param('id') id: number): Promise<Board> { this.logger.verbose(`Retrieving board with ID ${id}`); return this.boardsService.getBoardById(id); } // 특정 작성자의 게시글 조회 @Get('/search/:keyword') getBoardsByAuthor(@Query('author') author: string): Promise<Board[]> { this.logger.verbose(`Searching boards by author ${author}`); return this.boardsService.getBoardsByAuthor(author); } // 특정 번호의 게시글 삭제 @Delete('/:id') @Roles(UserRole.USER) // USER만 게시글 삭제 가능 deleteBoardById(@Param('id') id: number, @GetUser() user: User): void { this.logger.verbose(`User ${user.username} deleting board with ID ${id}`); this.boardsService.deleteBoardById(id, user); } // 특정 번호의 게시글의 일부 수정 (관리자가 부적절한 글을 비공개로 설정) // 커스텀 파이프 사용은 명시적으로 사용하는 것이 일반적 @Patch('/:id/status') @Roles(UserRole.ADMIN) updateBoardStatusById(@Param('id') id: number, @Body('status', BoardStatusValidationPipe) status: BoardStatus, @GetUser() user: User): void { this.logger.verbose(`Admin ${user.username} updating status of board ID ${id} to ${status}`); this.boardsService.updateBoardStatusById(id, status, user) } // 특정 번호의 게시글의 전체 수정 @Put('/:id') updateBoardById(@Param('id') id: number, @Body() updateBoardDto: UpdateBoardDto): void { this.logger.verbose(`Updating board with ID ${id}`); this.boardsService.updateBoardById(id, updateBoardDto) } }
TypeScript
복사
auth.controller.ts
import { Body, Controller, Logger, Post, Req, Res, UseGuards } from '@nestjs/common'; import { Response, Request } from 'express'; import { AuthService } from './auth.service'; import { CreateUserDto } from './dto/create-user.dto'; import { User } from './user.entity'; import { LoginUserDto } from './dto/login-user.dto'; import { AuthGuard } from '@nestjs/passport'; import { GetUser } from './get-user.decorator'; @Controller('api/auth') export class AuthController { private readonly logger = new Logger(AuthController.name); // Logger 인스턴스 생성 constructor(private authService: AuthService){} // 회원 가입 기능 @Post('/signup') // PostMapping 핸들러 데코레이터 signUp(@Body() createUserDto: CreateUserDto): Promise<User> { this.logger.verbose(`Attempting to sign up user with email: ${createUserDto.email}`); return this.authService.signUp(createUserDto); } // 로그인 기능 @Post('/signin') signIn(@Body() loginUserDto: LoginUserDto, @Res() res: Response) { this.logger.verbose(`Attempting to sign in user with email: ${loginUserDto.email}`); return this.authService.signIn(loginUserDto, res); } // 인증된 회원이 들어갈 수 있는 테스트 URL 경로 @Post('/test') @UseGuards(AuthGuard()) // @UseGuards : 핸들러는 지정한 인증 가드가 적용됨 -> AuthGuard()의 'jwt'는 기본값으로 생략가능 testForAuth(@GetUser() user: User) { this.logger.verbose(`Authenticated user accessing test route: ${user.email}`); return { message: 'You are authenticated', user: user }; } }
TypeScript
복사

3. AOP(Aspect-Oriented Programming)

AOP란?
AOP는 프로그램의 주요 로직과는 별개의 관심사를 모듈화하는 프로그래밍 패러다임
어떤 기능을 구현할 때 그 기능을 '핵심 기능''부가 기능'으로 구분해 각각 하나의 관점으로 보는기법을 의미
OOP에서 모듈화의 핵심 단위가 "클래스"라고 한다면, AOP에서는 핵심 단위가 "관점”이다.
AOP는 프로그램 구조에 대해 위와 같은 실무적인 사고 방식을 제공하여 객체 지향 프로그래밍(OOP)을 보완
AOP를 적용하는 주요 목적은 다음과 같다.
관심사 분리: AOP는 로깅, 보안, 트랜잭션 관리 등과 같은 공통 기능을 비즈니스 로직과 분리하여 관리하여 코드의 가독성과 유지보수성을 높임
재사용성: AOP를 사용하면 공통 기능을 재사용할 수 있는 모듈로 만들어 여러 클래스나 모듈에서 동일한 기능을 쉽게 적용할 수 있도록 함
코드 중복 제거: AOP는 코드 중복을 줄일 수 있음 예를 들어, 각 메서드에 로깅 코드를 삽입하는 대신 인터셉터를 사용하여 모든 메서드에 대해 자동으로 로깅을 적용
소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 하고 이러한 부분을 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 목적
Spring의 AOP와의 비교
스프링에서는 Aspect라는 직접적인 애너테이션(@데코레이터)를 제공하기 때문에 클래스를 선언하고 다양한 기능들을 커스텀하여 작성 할 수 있음
이에 따라 수행 기능(Advice)를 정의하고 해당 기능이 적용될 레벨(Target), 수행 시점(PointCut/JointPoint → Before,After, …ETC) 등을 설정하여 세밀한 조정을 할 수 있음
NestJS에서는 위 Aspect 같은 직접적인 데코레이터는 제공하고 있지 않음
하지만 프로젝트에 적용되는 주요 AOP 패턴들을 보다 쉽게 개발자가 사용 할 수 있도록 Guard, ValidationPipe 처럼 별도 기능처럼 제공하여 AOP(Aspect-Oriented Programming) 개념이 활용되고 있음
Guard는 요청 처리 과정에서 특정 조건을 검사하고, 그에 따라 요청을 허용하거나 거부하는 역할을 별도로 구성했었음
ValidationPipe도 DTO를 사용하여 요청 데이터를 구조화하고 유효성 검사하는 부분을 비즈니스 로직과의 관심사 분리를 통해 구성했었음
이 외에 Aspect 기능과 가장 유사하게 구성 할 수 있는 NestInterceptor 인터페이스를 제공함
요청과 응답을 가로채어 추가적인 작업(로깅, 변환, 캐싱 등)을 수행하도록 구성 할 수 있으므로 이는 AOP의 전후 처리 개념을 구현하는 데 유용하게 사용 되는 기능

3.1 Filter를 통한 전역 예외 관리 구현

전역 예외처리의 로그 추가
필터를 사용하여 전역으로 기본적인 각종 기본 예외 처리에 대한 부분을 관리 할 수 있다. 일부 프레임워크의 제어권에 있는 부분은 직접 로깅을 추가 할 수 없는데 전역으로 해당 부분의 로깅을 추가 할 수도 있다.
src/common/filters/unauthorization.filter.ts 생성
import { ExceptionFilter, Catch, ArgumentsHost, UnauthorizedException, Logger } from '@nestjs/common'; import { Response } from 'express'; @Catch(UnauthorizedException) export class UnauthorizedExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(UnauthorizedExceptionFilter.name); catch(exception: UnauthorizedException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const status = exception.getStatus(); this.logger.warn('Unauthorized access attempt detected'); response.status(status).json({ statusCode: status, message: exception.message, }); } }
TypeScript
복사
루트모듈 app.motule.ts 에서 로깅인터셉터 주입
import { Module } from '@nestjs/common'; import { BoardsModule } from './boards/boards.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { typeOrmConfig } from './configs/typeorm.config'; import { AuthModule } from './auth/auth.module'; import { GlobalModule } from './global.module'; import { APP_FILTER } from '@nestjs/core'; import { LoggingInterceptor } from './logging.interceptor'; @Module({ imports: [ GlobalModule, TypeOrmModule.forRoot(typeOrmConfig), BoardsModule, AuthModule ], providers: [ { provide: APP_FILTER, useClass: UnauthorizedExceptionFilter, }, ] }) export class AppModule {}
TypeScript
복사
예외 발생 시점의 로그 생성 확인

3.2 Interceptor를 통한 로깅 구현

전역 로그 생성
인터셉터를 사용하여 전역으로 기본적인 요청 및 응답 로깅을 처리하고, 특정 또는 세부적인 비즈니스 로직 관련 디테일을 살펴 볼 수 있는 로그는 각 메서드 내에서 로깅을 활용하는 방식이 가장 효율적이라 볼 수 있다.
src/common/interceptors/logging.interceptor.ts 생성
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); this.logger.log(`Request Method: ${method}, URL: ${url}`); return next.handle().pipe( tap(() => { const responseTime = Date.now() - now; this.logger.log(`Response for: ${method} ${url} - ${responseTime}ms`); }), ); } }
TypeScript
복사
루트모듈 app.motule.ts 에서 로깅인터셉터 주입
import { Module } from '@nestjs/common'; import { BoardsModule } from './boards/boards.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { typeOrmConfig } from './configs/typeorm.config'; import { AuthModule } from './auth/auth.module'; import { GlobalModule } from './global.module'; import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; import { LoggingInterceptor } from './logging.interceptor'; @Module({ imports: [ GlobalModule, TypeOrmModule.forRoot(typeOrmConfig), BoardsModule, AuthModule ], providers: [ { provide: APP_FILTER, useClass: UnauthorizedExceptionFilter, }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }, ] }) export class AppModule {}
TypeScript
복사
요청 및 응답 시점의 로그 생성 확인
Search
 | Main Page | Category |  Tags | About Me | Contact | Portfolio