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
복사
•
요청 및 응답 시점의 로그 생성 확인
Related Posts
Search