Service와 TypeORM

Pien·2023년 1월 7일
2

NestJS

목록 보기
3/8
post-thumbnail

Nest.js에서 서비스 단은 서버가 돌아가는 로직을 구현하는 곳이다. 데이터를 가공해 DB에 넣거나, DB의 데이터를 꺼내 가공 후 클라이언트에게 전달하기 전 작업과 같은 일을 한다.
데이터를 코드상으로 저장해 놓고 데이터를 사용하면 서버를 재실행할 때 마다 해당 데이터는 초기화가 되는데, 이를 방지하기 위해 DB와 연결해 주는 작업이 필요하다. 우리는 이 작업을 TypeORM을 사용할 것이다.

Service

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { QueryDto } from './dto/community-query.dto';
import { PostDetailDto, PostListDto, ResponsePostsDto } from './dto/response-post.dto';
import { Posts } from './entity/post.entity';
import { PostRepository } from './entity/post.repository';

@Injectable()
export class CommunityService {
	constructor(private readonly postRepository: PostRepository) {}

	async getPosts(GetPostListsDto: QueryDto): Promise<PostListDto> {
		return await this.postRepository.getManyAndCount(GetPostListsDto);
	}

	createPost(createPostDto: CreatePostDto): Promise<Posts> {
		return this.postRepository.save(createPostDto);
	}

	async updatePost(postId: number,updatePostDto: UpdatePostDto,
	): Promise<void> {
		const { userId } = updatePostDto;
		const postPermisson = await this.postRepository.findOneBy({id:postId, userId});

		if (!postPermisson)
			throw new HttpException("Don't have post permisson",HttpStatus.BAD_REQUEST);

		const result = await this.postRepository.update({ id: postId, ...updatePostDto});

		if (result.affected !== 1)
			throw new HttpException('INVALID ACCESS', HttpStatus.FORBIDDEN);

	return;
	}
}

기존 컨트롤러 예제의 의존성에 해당하는 서비스 단의 코드 예제이다. 서비스 단은 거의 모든 백엔드 프레임워크가 공통으로 가지고 있다. 서버의 기본적인 로직을 담당하며, 로직에 관련된 에러를 내보내거나, 데이터를 가공시켜 준다.

우리는 DB에 데이터를 저장하기 위해 TypeORM을 사용할 것이다. DB에 데이터를 넣는다는 것은 서비스 단에도 의존성을 주입해야 한다는 것이고, 서비스단은 Repository라는 TypeORM 저장소를 의존성을 주입해 사용할 것이다.

TypeORM

TypeORM은 Node진영에서 사용할 수 있는 ORM이다. TypeORM은 Active Record 패턴과 Data Mapper 패턴을 사용할 수 있다. Data Mapper 방식은 유지보수에 좋으며, 규모가 클수록 관리에 용이하다. 우리는 Data Mapper 방식을 사용할 것이다.

ORM

ORM(Object Relational Mapping)은 객체와 데이터베이스 간의 관계를 매핑해주는 도구다. ORM을 사용하면 객체의 관계를 토대로 SQL 쿼리문을 작성해 준다. 이를 통해 데이터베이스를 프로그래밍 언어적 관점으로 사용 할 수 있으며 가독성과 생산성, 유지보수의 편이성이 증가한다는 장점이 있다.

TypeORM 설치, 세팅

$ npm i --save mysql typeorm @nestjs/typeorm

Mysql기준으로 mysql 드라이버와 typeORM 모듈 두개를 설치해 준다. PostgreSQL이나 기타 다른 DB를 사용할 경우 해당 DB 드라이버를 설치하면 된다.

src/config/typeorm.config.ts 를 만들어 준 뒤 해당 코드를 작성해 주자

import { TypeOrmModuleOptions } from "@nestjs/typeorm"

export const typeORMConfig : TypeOrmModuleOptions = {
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'password',
    database: 'myDB',
    entities: [__dirname + '../**/*.entity.{js,ts}'],
    synchronize : true,
    logging: true
}

type 부터 database까지는 데이터베이스의 기본 정보를 입력해 주면 된다. entities 는 TypeORM에서 쓸 엔티티 들을 배열 안에 작성해 주면 되는데, 위 코드를 작성하면, 새로운 엔티티를 만들 때마다 추가하지 않고 알아서 엔티티를 찾아 준다.

synchronize는 서버를 실행할 때 엔티티가 변경될 때마다 테이블을 새로 만들어 준다. 테이블을 drop후 다시 생성해 주는 것이라, 데이터가 모두 삭제되니 조심히 사용하자!!

logging은 TypeORM이 작동할 때 마다 RawQuery문을 콘솔창에 보여준다. 오류를 찾거나 원하는 값이 나오지 않을 때 사용하면 좋다.

위 파일을 생성 후 작성해 줬으면 app.module.ts에 TypeORM 모듈을 import 시켜 데이터베이스와 연결해야 한다. import 시키지 않으면 DB와 연결된 게 아니다.

//  src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommunityModule } from './boards/boards.module';
import { typeORMConfig } from './configs/typeorm.config'

@Module({
  imports: [TypeOrmModule.forRoot(typeORMConfig), CommunityModule],
  providers: [],
})
export class AppModule {}

Entity

엔티티는 데이터베이스의 테이블을 정의하는 곳이다. 기본적인 컬럼을 작성하고 데이터 타입, 유니크 키, 상호 관계를 표시한다.

// src/community/entity/Posts.entity.ts
import { User } from 'src/user/entity/user.entity';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';

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

	@Column()
	title: string;

	@Column()
	description: string;

	@CreateDateColumn({ name : "createdAt"})
	created_at: Date;

	@Column()
	userId: number;

	@ManyToOne(() => User, (user) => user.post)
	@JoinColumn({ name: 'userId' })
	user: User;
}

// User Entity
	@OneToMany(() => Posts, (post) => post.user)
	post: Posts[];

컬럼은 데이터베이스의 컬럼명과 동일하게 작성하면 된다. 데이터베이스의 컬럼명과 다른 키값으로 쓰고 싶을 때는 원하는 키값을 적어 준 뒤, 옵션으로 데이터베이스의 컬럼 명을 적어주면 된다.

내 Posts 테이블은 id 값이 자동 생성되기 때문에 id 컬럼은 @PrimaryGeneratedColumn() 데코레이터를 사용했다.

그 외 엔티티에는 N : N 관계를 넣을 수 있다. 한 명의 유저는 여러 개의 포스트를 작성할 수 있다. 이는 1 : N 관계이며, N에 해당하는 곳에서 @JoinColumn을 작성해 주면 된다.

Repository

레파지토리는 엔티티와 같이 동작하며, 찾기, 생성, 삭제 등 데이터베이스에 대한 명령을 처리한다.
Controller > Service > Repository > Entity > DB 이 흐름이 Nest.js의 기본적인 흐름이다.

// src/community/entity/community.repository.ts
import { EntityRepository, Repository } from 'typeorm'
import { Posts } from './posts.entity'

@EntityRepository(Posts)
export class PostsRepository extends Repository<Posts> {
    
}

Repository 를 생성하고 만들어둔 Entity를 연결해 준다. 이를 통해 서비스단에 레파지토리 의존성을 주입할 수 있게 되었다.

의존성 주입을 통해 TypeORM에서 기본적으로 제공되는 레파지토리 인스턴스들을 서비스 단에서 사용할 수 있다.

기본적으로 제공되는 레파지토리 인스턴스나 세부화시키는 방법은 다음 포스팅에서 좀 더 자세히 설명할 예정이다.

프로젝트 폴더 구조

마치며

Nest.js의 기본적인 흐름을 간단하게 익혀 보았다. 너무 간략하게만 작성해 부족한 부분이 많아 보인다.
현재 코드는 서비스 단에서 레파지토리를 의존성 주입해 바로 DB에 데이터를 넣게 되어 있는데, 이는 내부 로직을 담당하는 서비스와 관련이 없다. 다음 포스팅은 레파지토리를 서비스 단에서 분리해 좀 더 구별감 있는 API를 만들어 볼 것이다.

2개의 댓글

comment-user-thumbnail
2023년 1월 17일

발도장 찍고 갑니다^-^

1개의 답글