본문 바로가기

Backend/NestJS

NestJS - 대충 서비스 만들어보기 (10)

지난번에 작성하지 못했던 Repository 를 연동해 광고 등록시 DB 에 저장하고 DB 로 부터 광고 데이터를 조회하는 API 를

완성해 보도록 하겠습니다.

 

- TypeORM Repository

 

NestJS 는 저장소 패턴을 지원하니 먼저 이를 사용해보도록 하겠습니다.

 

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('Ad')
export class Ad {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 10, name: 'ad_type' })
  adType: string;

  @Column({ length: 30 })
  title: string;

  @Column({ length: 60 })
  thumbnail: string;
  
  @Column({ length: 60 })
  content: string;

  @Column({ length: 2 })
  country: string;
}

 

크게 변경된건 없지만 먼저 entity 인 Ad 를 약간 수정했습니다.

누락했던 content 컬럼 추가와 adType 과 title 의 length 를 잘못 설정해서 약간 늘려주었습니다.

 

...
import { TypeOrmModule } from '@nestjs/typeorm';
import { Ad } from './entities/ad.entity';

@Module({
  imports: [
    ...,
    TypeOrmModule.forFeature([Ad]),
  ],
  ...
})
export class AdsModule {}

 

AdsModule 에서 사용할 저장소를 TypeOrmModule 의 forFeature 메서드로 등록해줍니다.

 

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateAdDto } from './dto/create-ad.dto';
import { UpdateAdDto } from './dto/update-ad.dto';
import { SlackService } from '../slack/slack.service';
import { Ad } from './entities/ad.entity';

@Injectable()
export class AdsService {
  constructor(
    private readonly slackService: SlackService,
    @InjectRepository(Ad) private adRepository: Repository<Ad>,
  ) {}

  async create(createAdDto: CreateAdDto) {
    const { adType, title, country, thumbnail, content } = createAdDto;
    const ad = new Ad();
    ad.adType = adType;
    ad.title = title;
    ad.country = country;
    ad.thumbnail = thumbnail;
    ad.content = content;

    await this.adRepository.save(ad);
    await this.slackService.send(adType, title, country);
    return 'This action adds a new ad';
  }

  ...
}

 

변경된 AdsService 부분입니다. @InjectRepository 데커레이터를 사용해 Ad 저장소를 주입하고,

기존 create 메서드에선 ad entity 객체를 생성한 후에 save 메서드를 통해 DB 에 해당 엔티티를 영구 저장합니다.

 

 

Postman 을 통해 POST API 를 호출해보면, 201 응답이 반환되는 것을 확인할 수 있습니다.

 

 

실제 DB 에도 정상 저장된 것을 확인할 수 있습니다.

 

...

@Injectable()
export class AdsService {
  ...

  findAll() {
    return this.adRepository.find();
  }

  findOne(id: number) {
    return this.adRepository.findOne({ where: { id } });
  }
}

 

간단하게 모든 광고 리스트를 조회하는 findAll 메서드와 단일 광고를 조회하는 findOne 메서드도 위처럼 작성을 해봤습니다.

 

 

두 API 에 대해서도 잘 동작하는 것을 확인했습니다.

응답 결과인 entity 를 그대로 넘기는 것은 지양해야 하지만 당장은 별다른 기능이 있는건 아니니 패스합니다.

 

- Custom Repository

 

기존 TypeORM 0.2 버전에선 제공하는 EntityRepository 데커레이터만 사용하면 custom repository 를 구현하기 아주 간단했습니다.

 

 

하지만 EntityRepository 데커레이터는 0.3 버전으로 넘어오면서 deprecated 되었습니다.

TypeORM 진영에서 EntityRepository 를 deprecated 해서 custom repository 생성을 비활성화 한 이유는.. 

일단 0.3 버전에서 custom repository 를 만든 후에 다시 적도록 하겠습니다.

 

import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Ad } from './entities/ad.entity';
import { CreateAdDto } from './dto/create-ad.dto';

export class AdsRepository extends Repository<Ad> {
  constructor(
    @InjectRepository(Ad) private readonly repository: Repository<Ad>,
  ) {
    super(repository.target, repository.manager, repository.queryRunner);
  }

  async createUser(adDto: CreateAdDto): Promise<null> {
    const { adType, title, country, thumbnail, content } = adDto;
    const ad = new Ad();
    ad.adType = adType;
    ad.title = title;
    ad.country = country;
    ad.thumbnail = thumbnail;
    ad.content = content;

    await this.repository.save(ad);

    return null;
  }

  async findAds(): Promise<Array<Ad>> {
    return await this.repository.find();
  }

  async findAdById(id: number): Promise<Ad> {
    return await this.repository.findOne({
      where: { id },
    });
  }
}

 

먼저 src/ads 하위에 ads.repository.ts 파일을 생성하고 위와 같이 작성해주었습니다.

typeorm 의 Repository 를 상속하는 AdsRepository 를 만듦으로써 0.2 때와 유사하게 custom repository 를 구현할 수 있게 되었습니다. 물론 기존의 @EntityRepository 를 사용했던 것 처럼 데코레이터로 만들려면 조금 다른 구현이 필요하지만 여기선 패스하겠습니다.

 

 

Repository 는 EntityTarget, EntityManager 필수 그리고 QueryRunner 를 옵셔널하게 생성자 인자로 받습니다.

AdsRepository 에서는 필요한 인자들을 부모 생성자에 넘겨주고 필요한 메서드들을 구현해주면 됩니다.

 

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdsService } from './ads.service';
import { AdsController } from './ads.controller';
import { SlackModule } from '../slack/slack.module';
import { Ad } from './entities/ad.entity';
import { AdsRepository } from './ads.repository';

@Module({
  imports: [SlackModule, TypeOrmModule.forFeature([Ad])],
  controllers: [AdsController],
  providers: [AdsService, AdsRepository],
})
export class AdsModule {}

 

약간은 변경된 AdsModule 입니다. 사실 추가된거라곤 AdsService 에서 AdsRepository 를 사용할 수 있도록 providers 에 새롭게

생성한 AdsRepository 를 추가한게 전부입니다.

 

import { Injectable } from '@nestjs/common';
import { AdsRepository } from './ads.repository';
import { CreateAdDto } from './dto/create-ad.dto';
import { SlackService } from '../slack/slack.service';

@Injectable()
export class AdsService {
  constructor(
    private readonly slackService: SlackService,
    private readonly adsRepository: AdsRepository,
  ) {}

  async create(createAdDto: CreateAdDto) {
    const { adType, title, country } = createAdDto;
    await this.adsRepository.createUser(createAdDto);
    await this.slackService.send(adType, title, country);
    return 'This action adds a new ad';
  }

  findAll() {
    return this.adsRepository.findAds();
  }

  findOne(id: number) {
    return this.adsRepository.findAdById(id);
  }
}

 

이제 AdsService 는 원래 @InjectRepository 데커레이터를 통해 주입받던 repository 에서 직접 생성한 AdsRepository 를 주입받는

코드로 변경되었습니다. 작성한 코드로 앱을 띄워 위에서 실행했던 API 들을 동일하게 수행해보면, 잘 동작하는 것을 확인할 수 있습니다.

 

앞에서 얘기하지 못한 부분들을 얘기하기 위해 custom repository 를 사용하기 전 AdsService 와 코드 비교를 좀 해보겠습니다.

 

// 기존 Repository 사용
findAll() {
  return this.adRepository.find();
}

findOne(id: number) {
  return this.adRepository.findOne({ where: { id } });
}

// custom repository 사용
findAll() {
  return this.adsRepository.findAds();
}

findOne(id: number) {
  return this.adsRepository.findAdById(id);
}

 

전체 광고를 조회하는 findAds 의 경우, custom repository 를 사용하더라도 해당 메서드에선 동일하게 repository 의 find 메서드를 호출해서 결과를 돌려주는게 전부입니다. 이런 경우 메서드를 굳이 만들 필요가 없음에도 @EntityRepository 데코레이터를 통해 custom repository 를 만들게되었고, 이것에 대해 여러 개발자들이 얘기를 나눈 결과 무분별한 사용으로 이어지게 되어 typeorm 측에서 이 데코레이터를 deprecated 했다고 합니다.

 

하지만 이는 지극히 간단한 예시에 불과하고, findOne 에서도 그렇지만 실제로 Repository 를 이용한 쿼리시에는 더 다양한 DB 연산을 날리게 되는데 이게 고스란히 Service 에 기술되는건 적절한 책임 분리로 보이진 않습니다. 따라서 현업에선 어차피 service layer 와 분리된 custom repository layer 를 작성하게 될거죠. 결국 판단은 자유지만 간단한 프로젝트라면 typeorm 의 repository 를 사용하는 것만으로 충분하고, 앱이 커지면서 필요한 기능들도 다양해진다면 custom repository 를 생성하는게 좋을 것 같습니다.