관리 메뉴

bright jazz music

[nestjs] DB연결을 위한 설정(config모듈 패키지, TypeORM 사용) 본문

Framework/NestJS

[nestjs] DB연결을 위한 설정(config모듈 패키지, TypeORM 사용)

bright jazz music 2024. 12. 21. 14:32

현재상황:

auth관련 디렉토리를 만들고 dto, controller, service 파일을 생성했다. 현재는 dto 만 작성 완료한 상태이다. 서비스와 컨트롤러의 로직은 아직 작성하지 않았다. 난 그것들의 로직을 작성하기 전에 먼저 데이터베이스를 연결하고 테이블을 생성하고 싶었다. ORM으로는  typeorm을 사용해 보려고 했다.

 

 

본문 요약:  DB 연결을 위한 설정

  1. postgreSQL과의 연결을 위한 TypeORM 패키지 설치
  2. 디비 설정을 환경변수 파일(.env.local)에 적어주고 깃 이그노어 하기(이건 프로젝트 생성 시 기본적으로 돼 있을 것이다)
  3. 설정을 위한 모듈 패키지(config) 설치하고 /src/에 config 디렉토리 생성
  4. /src/config디렉토리에 DB연결을 위한 설정파일(database.config.ts) 생성하고 내용 작성하기(환경변수 사용)
  5. 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
 

Node.js환경변수 설정과 dotenv(.env) 파일 관리

dotenv 파일 종류와 우선순위NestJS에서 환경변수 관리를 위한 dotenv 파일은 다음과 같은 종류가 있다..env: 기본 설정 파일.env.local: 로컬 개발 환경 전용 설정.env.development: 개발 환경 설정.env.production:

catnails.tistory.com

 

*여기서 앞에 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에 테이블 생성이 가능하고 엔티티에 정의된 대로 컬럼이 만들어진다. 아래와 같은 이유 때문이다.

  1. entities 경로가 설정되어 있고 ([__dirname + '/../**/*.entity{.ts,.js}'])
  2. synchronize가 true로 설정되어 있으며 (개발 환경일 경우)
  3. 데이터베이스 연결 정보가 모두 존재

 

예를 들어 아래와 같이 /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);
  }

}

 

Comments