일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- iterator
- d
- 페이징
- 자료구조와 함께 배우는 알고리즘 입문
- 스프링 시큐리티
- 스프링부트핵심가이드
- 서버설정
- 목록처리
- 구멍가게코딩단
- 알파회계
- network configuration
- 코드로배우는스프링웹프로젝트
- /etc/network/interfaces
- 자료구조와함께배우는알고리즘입문
- 이터레이터
- resttemplate
- 처음 만나는 AI수학 with Python
- GIT
- 친절한SQL튜닝
- 리눅스
- 선형대수
- 처음 만나는 AI 수학 with Python
- baeldung
- Kernighan의 C언어 프로그래밍
- 코드로배우는스프링부트웹프로젝트
- 자바편
- 티스토리 쿠키 삭제
- 네트워크 설정
- 데비안
- ㅒ
- Today
- Total
bright jazz music
[nestjs] DB연결을 위한 설정(config모듈 패키지, TypeORM 사용) 본문
[nestjs] DB연결을 위한 설정(config모듈 패키지, TypeORM 사용)
bright jazz music 2024. 12. 21. 14:32현재상황:
auth관련 디렉토리를 만들고 dto, controller, service 파일을 생성했다. 현재는 dto 만 작성 완료한 상태이다. 서비스와 컨트롤러의 로직은 아직 작성하지 않았다. 난 그것들의 로직을 작성하기 전에 먼저 데이터베이스를 연결하고 테이블을 생성하고 싶었다. ORM으로는 typeorm을 사용해 보려고 했다.
본문 요약: DB 연결을 위한 설정
- postgreSQL과의 연결을 위한 TypeORM 패키지 설치
- 디비 설정을 환경변수 파일(.env.local)에 적어주고 깃 이그노어 하기(이건 프로젝트 생성 시 기본적으로 돼 있을 것이다)
- 설정을 위한 모듈 패키지(config) 설치하고 /src/에 config 디렉토리 생성
- /src/config디렉토리에 DB연결을 위한 설정파일(database.config.ts) 생성하고 내용 작성하기(환경변수 사용)
- app.module.ts에 설정모듈(config모듈)과 TypeORM 모듈 등록하고 설정해주기
1. postgreSQL 과의 연결을 위한 TypeORM 패키지 설치
pnpm add @nestjs/typeorm typeorm pg
2. 루트 디렉토리(/)에 환경변수 파일인 .env .local 생성하고 아래와 같이 입력
DB_HOST=20.20.20.21
DB_PORT=5432
DB_USERNAME=test_user
DB_PASSWORD=your_password
DB_NAME=test_db
*여기서 앞에 DB_가 붙은 녀석들은 변수명이다. 따라서 반드시 지킬 필요는 없다. 임의이 이름으로 해줘도 문제 없다.
DB_HOST
: 사용할 디비가 설치돼 있는 서버의 아이피 주소를 적어주면 된다.
여기서는 예시로 20.20.20.21을 적었지만 회사나 집의 사설망에 존재하는 개발서버의 주소는 192.168.0.57 등으로 돼 있는 게 일반적일 것이다. 만약 애플리케이션을 개발하는 컴퓨터에서 db도 운영하고 있다면 127.0.0.1 또는 localhost를 적어주면 될 것이다.
DB_PORT
: 포트는 DBMS의 종류에 따라 다르며 종류가 같더라도 관리자가 포트를 변경해 줬다면 또 다르다. 여기서는 postgreSQL을 사용하기 때문에 postgreSQL의 기본 포트인 5432로 해주었다. 만약 mysql이나 mariaDB를 사용한다면 기본 포트는 3306으로 해줘야 할 것이다. (사실 개발용이라도 기본 포트를 사용하지 않고 임의의 포트로 사용하느 것이 좋기는 하다)
DB_NAME
: 사용할 DB의 이름을 적어준다. postgreSQL은 DBMS이지 DB가 아니다. 만약 아직 DB를 만들지 않았다면 DBMS에서 CREATE DATABASE 등의 명령어로 DB를 만들어 준 다음 해당 DB의 이름을 적어주면 된다.
DB_USERNAME
: 해당 DB를 사용할 수 있는 권한이 있는 사용자이다. 이 애플리케이션에선 해당 사용자의 계정으로 DB에 접근해 데이터를 조작한다. 물론 이 역시 DBMS에서 해당 사용자를 생성해 주고, 연결하려는 DB에 대한 권한을 부여하는 과정이 선행되어야 한다. 뭐 여기에 적어주고 DBMS에서 만들어줘도 되고. 그래도 DBMS에서 먼저 사용자 계정을 만들어주는 편이 일반적인 것 같다.
DB_PASSWORD
: 사용자 계정이 DB에 접근할 때 사용하는 비밀번호.
3. nestjs의 config모듈 패키지 설치 후 config 디렉토리 생성
3.1. 설정관리 위한 config 모듈 패키지 설치
상식적으로 NestJS에서도 설정을 분리하는 관리하는 것이 좋다. 보통 @nestjs/config 패키지를 사용하여 환경 변수와 설정을 관리한다.
pnpm add @nestjs/config
// npm을 사용한다면 npm isntall @nestjs/config
3.2. config 디렉토리 생성 (/src/config)
/src 디렉토리에 config 라는 이름의 디렉토리를 생성한다. database.config.ts는 내가 config디렉토리를 만든 후에 그 안에서 미리 생성해 준 것이다. 아래 4번 항목에 자세히 적어 놓았으니 일단은 config 디렉토리부터 만들자.
앞으로 여기에 설정파일을 만들고, 추후 app.module.ts에 config모듈을 등록시키고, 로드 배열에 설정 파일을 추가해 줄 것이다. 아래 과정을 따라가면 된다.
4. config 디렉토리에 database를 위한 설정 파일 생성하고 내용 작성
/src/config/에 database.config.ts 파일을 생성한다. db연결을 위한 설정을 작성하는 파일이다. database.config.ts는 환경변수(.env 파일, 여기서는 env.local 파일)의 값들을 TypeORM이 이해할 수 있는 형태의 설정 객체로 변환하는 역할을 한다. registerAs를 통해 'database'라는 네임스페이스로 묶어서 다른 모듈에서 손쉽게 참조할 수 있게 만드는 것이다.
* database.config.ts파일 자체가 환경설정 파일은 아니다. 환경설정 파일은 .env등의 파일을 의미한다. database.config.ts는 환경변수 파일의 환경변수를 가져와서 모듈(함수)로 만들어 값을 반환하게 만드려고 사용하는 것이다. 굳이 이름을 붙이자면 "DB연결 설정 모듈 파일"이라고 할 수 있겠다.
* 싱크로나이즈 부분을 주의깊게 읽을 것.
import { registerAs } from '@nestjs/config';
/**
* NestJS 데이터베이스 설정 파일
* registerAs를 사용해 'database' 네임스페이스로 설정을 등록
* 등록된 설정은 ConfigService를 통해 'database.host'와 같은 방식으로 접근 가능
*/
export default registerAs('database', () => ({
// PostgreSQL을 사용하도록 지정
type: 'postgres',
// 환경변수에서 데이터베이스 연결 정보를 가져옴
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// 엔티티 파일의 위치를 지정
// 현재 디렉토리 기준으로 모든 하위 폴더에서 .entity.ts 또는 .entity.js 파일을 찾음
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
/**
* synchonize 주의! 운영환경(production)에서는 false로 되어야 한다.
* 개발 환경에서는 true로 해서 사용할 예정.
* TypeORM의 자동 스키마 동기화 설정
* 개발 환경(NODE_ENV !== 'production')에서는 true: 엔티티 정의에 따라 DB 스키마 자동 업데이트
* 프로덕션 환경에서는 false:
* - 의도치 않은 데이터 손실 방지
* - 스키마 변경은 마이그레이션으로 명시적 관리
* - 자동 동기화로 인한 성능 저하 방지
*
* 실행 예시:
* - 개발 환경: NODE_ENV=local pnpm run:dev => synchronize: true
* - 운영 환경: NODE_ENV=production pnpm run start => synchronize: false
*
* 주의: 실수로 운영환경에서 true로 설정되면 데이터베이스 스키마가 자동으로 변경될 수 있으므로
* 배포 전 반드시 확인이 필요함
*/
synchronize: process.env.NODE_ENV !== 'production',
}));
////////////////////////////////////////////////////////
/*
프로젝트에서 구체적인 값을 배제하려고 .env를 쓰는 건데 아래처럼 하면 정보의 일부라도 보여진다. 난 그게 싫어서 위처럼 사용
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
type: 'postgres',
host: process.env.DB_HOST || '20.20.20.21',
port: parseInt(process.env.DB_PORT, 10) || 5432,
username: process.env.DB_USERNAME || 'test_user',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'test_db',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production',
}));
*/
.env.local 파일에 작성한 환경변수를 사용하는 것을 볼 수 있다.
5. app.modules.ts에서 임포트해서 사용
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; // 우리가 설치한 설정 모듈
import { TypeOrmModule } from '@nestjs/typeorm'; // 타입오알엠 임포트
import { AuthModule } from './auth/auth.module';
import databaseConfig from './config/database.config'; // 방금 작성한 디비설정 임포트
@Module({
imports: [
// 우리가 아까 설치했던 config 모듈이다. 적어주자.
ConfigModule.forRoot({
isGlobal: true, // 이 설정을 전역적으로 사용한다는 의미
load: [databaseConfig], // 여기에 디비설정 등록. database.config.ts에서 만든 설정을 여기 등록
}),
// TypeORM 설정을 비동기로 등록
TypeOrmModule.forRootAsync({
inject: [ConfigService], // ConfigService를 주입받아서
useFactory: (configService: ConfigService) => ({
...configService.get('database'), // database.config.ts에서 등록한 'database' 네임스페이스의 설정을 가져옴
}),
}),
AuthModule, // 이전에 작성한 auth모듈
],
})
export class AppModule {}
/*
아래처럼 직접 값을 임포트해서 사용하는 것은 좋지 않다.
위처럼 환경변수를 사용해서 값을 이용하자.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: '20.20.20.21',
port: 5432,
username: 'test_user',
password: 'your_password', // 실제 비밀번호를 입력해주세요
database: 'test_db',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // 개발 환경에서만 true로 설정. 프로뎍선에선 폴스
}),
AuthModule,
],
})
export class AppModule {}
*/
6. 마무리
DB연결을 위한 기본적인 설정이 끝났다. 민감한 정보를 소스코드에 직접 기록(하드코딩)하지 않고 환경설정 파일을 따로 만들어 관리하면 아래와 같은 장점이 있다.
- 민감한 정보(비밀번호, 계정정보, 주소정보)를 소스코드에서 분리할 수 있다.
- 설정 값들을 변수화하여 관리하므로 중앙에서 관리할 수 있다.
- 정보의 값이 바뀌게 되어도 설정파일에서만 변경하면 되므로 간편하다.
TypeORM을 여기서 사용한 이유는 아래와 같다.
TypeORM을 사용하는 이유:
1. 객체 지향적 데이터 관리
- 데이터베이스 테이블을 TypeScript/JavaScript 클래스로 표현 가능
- 객체와 관계형 데이터베이스 간의 매핑을 자동으로 처리
2. 데이터베이스 독립성
- PostgreSQL, MySQL, SQLite 등 다양한 데이터베이스 지원
- 데이터베이스 변경 시 코드 수정 최소화
3. TypeScript 지원
- 강력한 타입 지원으로 개발 시 타입 안정성 확보
- IDE의 자동 완성 기능 활용 가능
4. NestJS와의 통합
- NestJS의 공식 권장 ORM
- @nestjs/typeorm 패키지로 쉬운 통합 가능
- 의존성 주입(DI) 시스템과 잘 동작
5. 강력한 쿼리 빌더
- 복잡한 SQL 쿼리를 타입스크립트 코드로 작성 가능
- 체이닝 방식의 직관적인 쿼리 작성
6. 마이그레이션 지원
- 데이터베이스 스키마 변경사항을 코드로 관리
- 버전 관리와 롤백 가능
어쨌든 여기가지 했으면 프로젝트의 src 디렉토리에 엔티티 파일만 있어도 DB에 테이블 생성이 가능하고 엔티티에 정의된 대로 컬럼이 만들어진다. 아래와 같은 이유 때문이다.
- entities 경로가 설정되어 있고 ([__dirname + '/../**/*.entity{.ts,.js}'])
- synchronize가 true로 설정되어 있으며 (개발 환경일 경우)
- 데이터베이스 연결 정보가 모두 존재
예를 들어 아래와 같이 /src/user.entity.ts 파일이 존재하는 경우 test_db에 users 테이블이 자동으로 생성되고 id, email 컬럼이 만들어진다. 앞서 말했듯 이는 synchronize: true일 때만 해당하며, 프로덕션 환경에서는 이런 자동 생성을 피하기 위해 synchronize을 false로 설정해야 한다.
@Entity()
export class Member {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
}
// @Entity()에 파라미터가 없으므로 클래스명인 member로 테이블이 만들어진다.
// 파라미터를 넣으면 클래스명과 무관하게 파라미터명으로 테이블이 생성된다.
// 예를 들어 @Entity("players)로 하면 players라는 테이블이 만들어진다.
// postgreSQL에서는 user라는 이름을 피하는 것이 좋다. 예약어이기 때문이다. 사용은 가능하지만 번거롭다.
// PostgreSQL에서는 snake_case를 선호하므로, 아래와 같이 설정할 수도 있다
// @Entity('tb_members') // 테이블명에 접두어를 붙이는 컨벤션을 사용하는 경우
위처럼 엔티티 파일을 만들고, NODE_ENV=local pnpm run:dev 등의 명령어로 프로젝트를 구동하여 테이블이 만들어지는지 확인해봐도 좋다.
디티오 추가
// src/users/dto/create-user.dto.ts
// 유저 생성 DTO
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
업데이트 디티오
// src/users/dto/create-user.dto.ts
// 유저 생성 DTO
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
2. 엔티티 만들기
유저와 관련된 기능부터 만들고 그 다음 auth를 만든다.
도메인 분리의 관점에서 보면 User 엔티티는 별도의 users 모듈에 있는 것이 더 적절합니다. 다음과 같이 구조를 변경하는 것을 추천드립니다:
// src/users/entities/user.entity.ts
// 유저 엔티티
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
3. 응답 디티오 만들기
// src/users/dto/user-response.dto.ts
// 유저 응답 DTO
export class UserResponseDto {
id: number;
email: string;
createdAt: Date;
updatedAt: Date;
}
유저모듈에 타입오알엠과 함께 엔티티 등록
// src/users/users.module.ts
// 유저 모듈
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // 타입오알엠
import { User } from './entities/user.entity'; // 방금 생성한 유저 엔티티
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])], // 타입 오알엠과 유저 엔티티
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
/*
기존에는 아래와 같았음.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
*/
app.modules.ts에 유저 모듈도 추가
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module'; // 유저모듈도 추가
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig],
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
...configService.get('database'),
}),
}),
UsersModule, // 유저모듈 추가
AuthModule,
],
})
export class AppModule {}
유저 컨트롤러와 기본적인 씨알유디. 이건 기본적인 설정만 해둔 것이다. 아래에서 서비스 작성과 디티오 수정을 하면서 또 내용이 바뀔 예정.
import { Controller, Get, Put, Delete, Param, Body } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
async findOne(@Param('id') id: number) {
return this.usersService.findOne(id);
}
@Put(':id')
async update(@Param('id') id: number, @Body() updateUserDto: any) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
async remove(@Param('id') id: number) {
return this.usersService.remove(id);
}
// 관리자용 엔드포인트
@Get()
async findAll() {
return this.usersService.findAll();
}
}
이렇게 하면:
- /auth로 시작하는 엔드포인트는 회원가입/로그인 등 인증 관련 기능을 처리
- /users로 시작하는 엔드포인트는 사용자 정보 관리 기능을 처리
이런 식으로 역할이 명확하게 구분됩니다.제가 앞서 컨트롤러를 제거한 것은 너무 서비스 로직에만 집중한 실수였네요. API 서버를 만드는 것이 목적이라면 당연히 컨트롤러가 필요합니다! 😊
----
지금까지 진행한 것들
- 데이터베이스 설정
- ConfigModule을 사용한 환경 변수 설정
- TypeORM 연결 설정
- .env 파일 생성
2.모듈 구조 설정
- Users 모듈: 사용자 정보 관리
- Auth 모듈: 인증 관련 처리
3. 엔티티 설정
- User 엔티티 생성 (users 모듈로 이동)
4.컨트롤러 설정
- UsersController 기본 CRUD 엔드포인트 구성
다음으로 해야 할 작업들은:
- UsersService 구현
- TypeORM Repository를 사용한 실제 데이터베이스 작업 구현
- AuthService 구현
- 회원가입/로그인 로직 구현
- JWT나 세션 기반 인증 구현
- DTO 작성
- UpdateUserDto 등 필요한 DTO 작성
유저 서비스 수정
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return await this.usersRepository.find();
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async findByEmail(email: string): Promise<User | null> {
return await this.usersRepository.findOne({ where: { email } });
}
async update(id: number, updateData: Partial<User>): Promise<User> {
const user = await this.findOne(id);
Object.assign(user, updateData);
return await this.usersRepository.save(user);
}
async remove(id: number): Promise<void> {
const result = await this.usersRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`User with ID ${id} not found`);
}
}
}
기존 유저 디티오들 수정
// import { PartialType } from '@nestjs/mapped-types';
// import { CreateUserDto } from './create-user.dto';
import { IsEmail, IsOptional, IsString, MinLength } from 'class-validator';
export class UpdateUserDto {
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsString()
@MinLength(8)
password?: string;
}
import { Controller, Get, Put, Delete, Param, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { UpdateUserDto } from './dto/update-user.dto'; // 업데이트 유저 디티오
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
async findOne(@Param('id') id: number) {
return this.usersService.findOne(id);
}
@Put(':id') // 기존 애니 타입에서 UpdateUserDto로 타입이 변경됨
async update(@Param('id') id: number, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
async remove(@Param('id') id: number) {
return this.usersService.remove(id);
}
// 관리자용 엔드포인트
@Get()
async findAll() {
return this.usersService.findAll();
}
}
응답 디티오 만들기
// src/users/dto/user-response.dto.ts
// 유저 응답 DTO
export class UserResponseDto {
id: number;
email: string;
createdAt: Date;
updatedAt: Date;
}
이렇게 하면:
- TypeORM Repository를 사용한 기본적인 CRUD 작업
- 이메일로 사용자 찾기 기능 (auth 모듈에서 사용할 예정)
- class-validator를 사용한 DTO 유효성 검사
- 존재하지 않는 사용자에 대한 예외 처리
이제 리포지토리를 만들자
유저리포지토리 생성
// src/users/repositories/user.repository.ts
// 유저 리포지토리
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { User } from '../entities/user.entity';
@Injectable()
export class UserRepository {
private repository: Repository<User>;
constructor(private dataSource: DataSource) {
this.repository = this.dataSource.getRepository(User);
}
async findOne(id: number): Promise<User | null> {
return this.repository.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.repository.findOne({ where: { email } });
}
async findAll(): Promise<User[]> {
return this.repository.find();
}
async create(user: Partial<User>): Promise<User> {
const newUser = this.repository.create(user);
return this.repository.save(newUser);
}
async save(user: User): Promise<User> {
return this.repository.save(user);
}
async delete(id: number): Promise<{ affected?: number }> {
return this.repository.delete(id);
}
}
매퍼 만들기
디티오와 엔티티를 변환해주는 역할을 함
// src/users/mappers/user.mapper.ts
// 유저매퍼
import { User } from '../entities/user.entity';
import { UserResponseDto } from '../dto/user-response.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
export class UserMapper {
static toDto(user: User): UserResponseDto {
const dto = new UserResponseDto();
dto.id = user.id;
dto.email = user.email;
// password는 제외
dto.createdAt = user.createdAt;
dto.updatedAt = user.updatedAt;
return dto;
}
static toDtoList(users: User[]): UserResponseDto[] {
return users.map(user => this.toDto(user));
}
static toEntity(dto: CreateUserDto | UpdateUserDto): Partial<User> {
const entity = new User();
if (dto.email) entity.email = dto.email;
if (dto.password) entity.password = dto.password;
return entity;
}
}
모듈 등록
// src/users/users.module.ts
// 유저 모듈
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { UserRepository } from './repositories/user.repository';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, UserRepository],
exports: [UsersService],
})
export class UsersModule {}
이렇게 구성하면:
- Repository 패턴을 통한 데이터 접근 계층 분리
- DTO를 통한 데이터 전송 객체 분리
- Mapper를 통한 변환 로직 중앙화
- 응답 데이터의 일관성 보장
보완
타입 안정성 위해 타입 추가
// src/users/interfaces/user.interface.ts
// 유저 인터페이스
export interface IUser {
id: number;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
예외 필터 추가
// src/common/filters/http-exception.filter.ts
// HTTP 예외 필터
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const error = exception.getResponse();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
error: error,
});
}
}
에러메시지 상수로 관리
// src/common/constants/error-messages.constant.ts
// 에러 메시지 상수
export const ERROR_MESSAGES = {
USER_NOT_FOUND: 'User not found',
EMAIL_ALREADY_EXISTS: 'Email already exists',
// ... 기타 에러 메시지
} as const;
서비스에서 에러메시지 상수로 사용하는 것으로 변경
import { Injectable, NotFoundException } from '@nestjs/common';
import { UserRepository } from './repositories/user.repository';
import { UserMapper } from './mappers/user.mapper';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserResponseDto } from './dto/user-response.dto';
import { DataSource } from 'typeorm';
import { ERROR_MESSAGES } from '../common/constants/error-messages.constant';
@Injectable()
export class UsersService {
constructor(
private readonly userRepository: UserRepository,
private readonly dataSource: DataSource,
) {}
async findAll(): Promise<UserResponseDto[]> {
const users = await this.userRepository.findAll();
return UserMapper.toDtoList(users);
}
async findOne(id: number): Promise<UserResponseDto> {
const user = await this.userRepository.findOne(id);
if (!user) {
throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
}
return UserMapper.toDto(user);
}
async findByEmail(email: string): Promise<UserResponseDto | null> {
const user = await this.userRepository.findByEmail(email);
return user ? UserMapper.toDto(user) : null;
}
async update(id: number, updateData: UpdateUserDto): Promise<UserResponseDto> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = await this.userRepository.findOne(id);
if (!user) {
throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
}
const updatedUser = await this.userRepository.save({
...user,
...updateData,
});
await queryRunner.commitTransaction();
return UserMapper.toDto(updatedUser);
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
async remove(id: number): Promise<void> {
const result = await this.userRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
}
}
}
컨트롤러도 응답디티오와 파라미터 검증 추가
// src/users/users.controller.ts
// 유저 컨트롤러
import {
Controller,
Get,
Put,
Delete,
Param,
Body,
ParseIntPipe,
HttpStatus,
HttpCode
} from '@nestjs/common';
import { UsersService } from './users.service';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserResponseDto } from './dto/user-response.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(): Promise<UserResponseDto[]> {
return this.usersService.findAll();
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number): Promise<UserResponseDto> {
return this.usersService.findOne(id);
}
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
): Promise<UserResponseDto> {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.usersService.remove(id);
}
}
'Framework > NestJS' 카테고리의 다른 글
[nestjs] TypeORM을 사용해 DB에 테이블 생성 (0) | 2024.12.21 |
---|---|
[nestjs] Repository pattern으로 변경하기 (0) | 2024.12.21 |
[nestjs] 2.1. auth구현 (1) : 회원가입,로그인,로그아웃 (1) | 2024.12.19 |
[nestjs] 1. 테스트 프로젝트 생성 (0) | 2024.12.19 |
nestjs의 Pipe (1) | 2024.11.10 |