[Node.js_4기] TIL : Nest.js 3_AOP와 캐싱, (24/03/15)

2024. 3. 17. 23:54공부/내배캠 TIL

목차

 

1. 학습 내용

2. 내용 정리

3. 예제

4. 생각 정리

 

1. 학습 내용 

 

  • AOP 개념에 대해서 확실히 이해를 하고 Nest.js에서 AOP 개념을 인터셉터를 활용하여 구현할 수 있습니다.
  • 캐시를 적용하여 서버의 성능을 최적화 할 수 있습니다.

 

2. 내용 정리 

04. AOP

04_01) AOP(Aspect-Oriented Programming) 

관심사분리 : 여러 부분에서 반복되는 관심사를 분리하고 중앙에서 관리할 수 있게 프로그래밍 하는 것

  • 코드의 모듈성을 향상시키고 중복을 줄일 수 있다.

프록시 객체

  • JS 내장객체, 다른 객체의 기본 동작을 수정 가능.(로깅, 인증, 에러처리 등...)

헬퍼 객체

  • 특정 작업을 수행하는 메서드를 정의하는 것을 통해 코드의 재사용성을 높일 수 있다.
  • 공통 기능을 모듈화하고 여러 부분에서 호출 가

전통적 프로그래밍 방식과의 비교

  • 전통적 프로그래밍 방식
    • 각 방을 따로 만들고 각 방에 개별적으로 문을 설치
    • 만약 모든 문의 디자인을 바꾸려면 모든 방의 문을 각각 변경해야함
  • AOP 방식
    • 문을 설계하고 제작하는 작업을 별도의 공통 모듈에서 수행하고 이를 모든 방에 적용
    • 이렇게 하면 문의 디자인을 한 곳에서 변경하면 모든 방의 문이 자동으로 업데이트됩니다.
  •  로깅이나 인증, 에러 처리와 같은 기능들은 사실 어플리케이션에서 계속 공통적으로 필요로 하는 기능들
  • 이러한 기능들을 각각의 코드 블록에 반복해서 작성하는 것은 비효율적이고 에러를 유발
    • 그러므로 AOP 방식을 도입

04_2) 인터셉터

특정 작업을 수행하기 전이나 후에 추가 로직을 실행할 수 있는 코드 블록(AOP 개념을 구현하는 핵심 요소)

  • 특히, HTTP 요청과 응답을 처리할 때 특히 유용
  • 주로 로깅, 에러 처리, 데이터 변환 및 인증과 같은 공통 관심사를 처리하는 데 사용

05. Nest.js에서의 캐싱

05_1) 캐싱

자주 변하지 않는 데이터에 동일한 요청이 지속적으로 들어오는 경우에 대해서는 캐싱 기능을 사용할 수 있다면 서버의 성능이 전반적으로 올라간다. (캐싱은 사용할 수 있으면 무조건 사용하는 것이 성능을 끌어올릴 수 있다)

  • cache-manager와 연계를 하여 캐싱 기능을 사용

 

3. 예제 

 

04_2) 인터셉터 

import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();

    return next
      .handle()
      .pipe(tap(() => console.log(`완료에 걸린 시간: ${Date.now() - now}ms`)));
  }
}

NestInterceptor 인터페이스를 구현하는 것이 필수입니다.

  • intercept라는 메소드를 통해 요청을 가로챈 후 next.handle()을 호출한다는 것 = 요청을 처리하고 응답을 반환하는 컨트롤러 메소드로 이동을 한다는 뜻(요청이 처리된 이후임)
  • pipe()라는 함수로 요청 처리 파이프라인의 다음단계로 이동을 한 후 tap()이라는 연산자 내에서 응답이 처리된 시간을 로그로 찍는 것

05_2) 캐싱

<app.modules.ts>
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [
  ...
    CacheModule.register({
          ttl: 60000, // 데이터 캐싱 시간(밀리 초 단위, 1000 = 1초)
          max: 100, // 최대 캐싱 개수
          isGlobal: true,
        }),
  ....
  ]
})

register를 할 때 ttl, max, isGlobal 옵션을 설정하여 원하는 방식으로 캐싱 가능

<post.service.ts>
import { Cache } from 'cache-manager';
import _ from 'lodash';
import { Repository } from 'typeorm';

import { CACHE_MANAGER } from '@nestjs/cache-manager';
import {
  BadRequestException, Inject, Injectable, NotFoundException, UnauthorizedException
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { CreatePostDto } from './dto/create-post.dto';
import { RemovePostDTO } from './dto/remove-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Post } from './entities/post.entity';

@Injectable()
export class PostService {
  constructor(
    @InjectRepository(Post) private postRepository: Repository<Post>,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {}

  async create(createPostDto: CreatePostDto) {
    return (await this.postRepository.save(createPostDto)).id;
  }

  async findAll() {
    const cachedArticles = await this.cacheManager.get('articles');
    if (!_.isNil(cachedArticles)) {
      return cachedArticles;
    }

    const articles = await this.postRepository.find({
      where: { deletedAt: null },
      select: ['id', 'title', 'updatedAt'],
    });
    await this.cacheManager.set('articles', articles);
    return articles;
  }

  async findOne(id: number) {
    if (_.isNaN(id)) {
      throw new BadRequestException('게시물 ID가 잘못되었습니다.');
    }

    return await this.postRepository.findOne({
      where: { id, deletedAt: null },
      select: ['title', 'content', 'updatedAt'],
    });
  }

  async update(id: number, updatePostDto: UpdatePostDto) {
    if (_.isNaN(id)) {
      throw new BadRequestException('게시물 ID가 잘못되었습니다.');
    }

    const { content, password } = updatePostDto;
    const post = await this.postRepository.findOne({
      select: ['password'],
      where: { id },
    });

    if (_.isNil(post)) {
      throw new NotFoundException('게시물을 찾을 수 없습니다.');
    }

    if (!_.isNil(post.password) && post.password !== password) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }

    await this.postRepository.update({ id }, { content });
  }

  async remove(id: number, removePostDto: RemovePostDTO) {
    if (_.isNaN(id)) {
      throw new BadRequestException('게시물 ID가 잘못되었습니다.');
    }

    const { password } = removePostDto;

    const post = await this.postRepository.findOne({
      select: ['password'],
      where: { id },
    });

    if (_.isNil(post)) {
      throw new NotFoundException('게시물을 찾을 수 없습니다.');
    }

    if (!_.isNil(post.password) && post.password !== password) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }

    await this.postRepository.softDelete({ id });
  }
}

위의 코드는 다음과 같은 흐름을 따른다.

  • 캐시를 확인한다
    • 캐시가 히트되었다면 캐시를 리턴
    • 캐시가 미스되었다면
      • Repository를 통해 DB에 접근하여 새로운 결과물을 가져온다
      • 결과물을 캐싱한다
      • 결과물을 리턴한

 

4. 생각 정리 

 

해당 TIL을 마지막으로, Nest.js TIL을 마칩니다.

다음 TIL로 프로젝트 회고와 해설 영상을 통해 배운 내용을 정리할 예정입니다.