Repository 패턴이란?
Repository 패턴은 데이터 소스 간의 중간 계층을 제공하여 데이터 액세스 로직과 비즈니스 로직을 분리하는 디자인 패턴입니다.
이 패턴을 사용하면 애플리케이션이 사용하는 데이터 소스의 실제 구현에 관계없이 일관된 방법으로 데이터를 조작할 수 있습니다.
주요 목적은 데이터 소스에 대한 직접적인 접근 대신 특정 엔티티에 대한 모든 데이터 액세스 로직을 캡슐화하여 애플리케이션과 데이터베이스 사이의 결합도를 낮추는 것입니다.
Repository 패턴의 역할
repository는 데이터 액세스 로직을 처리합니다. 즉 데이터베이스와 관련된 로직을 처리하고 service는 비즈니스 로직을 처리합니다.
CRUD 연산 : 기본 데이터베이스 연산을 추상화하고 이를 통해 개발자는 복잡한 쿼리를 작성하지 않고도 필요한 데이터를 쉽게 조작할 수 있습니다.
Entity 관리 : 특정 Entity 타입에 대한 모든 데이터베이스 작업을 관리합니다. 각 Entity에 대한 별도의 Repository를 생성하여 Entity 간의 경계를 명확하게 합니다.
쿼리 로직의 캡슐화 : 복잡한 쿼리 로직을 Repository 내부에 캡슐화함으로써 비즈니스 로직에서는 필요한 데이터를 간단한 메소드 호출로 얻을 수 있습니다.
데이터베이스 세션 관리 : TypeORM에서 Repository는 데이터베이스 연결과 트랜잭션 관리를 담당하여 데이터베이스 작업을 보다 안정적으로 수행할 수 있게 해 줍니다.
Repository 패턴을 적용한 간단한 로그인 기능구현
간단한 기능은 Service에서 모든 데이터베이스 요청을 처리하여 사용할 수 있지만 이번 실습에서는 역할을 알아보기 위해 집중하겠습니다.
로그인 기능 같은 경우 인증 및 검증이 필요한 부분이지만 이번 실습에서는 repository 패턴을 알아보기 위해 간단한 로직으로 구현했습니다.
// typeorm.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const TypeOrmConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'sample_db',
entities: [__dirname + '/../**/*.entity.{js,ts}'],
synchronize: true,
};
// app.module.ts
import { Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { TypeOrmConfig } from './config/typeorm.config';
@Module({
// TypeOrmConfig 설정을 가져옴
// 저번시간에는 이부분에 내용을 직접 넣어주는 방식을 사용했지만 이번에는 다른 파일에서 불러오는 방식사용
imports: [TypeOrmModule.forRoot(TypeOrmConfig), UserModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
// @Entity 데코레이터를 사용하여 이 클래스가 데이터베이스 테이블에 매핑되는 엔티티임을 선언
// 데코레이터에 테이블 이름을 명시적으로 지정하지 않으면 클래스 이름을 카멜 케이스로 자동 변환하여 테이블 이름으로 사용
// 단점으로 테이블 이름 간의 연결을 코드에서 직접 추적해야 하기 때문에 관리 포인트가 늘어날 수 있음
@Entity()
export class User {
// @PrimaryGeneratedColumn 데코레이터는 이 필드가 기본 키이며 자동으로 생성되는 값을 의미
@PrimaryGeneratedColumn()
id: number;
// @Column 데코레이터는 일반 컬럼을 의미
// @Column({ unique: true }) 이런식으로도 중복을 방지 할 수 있음
@Column()
username: string;
@Column()
password: string;
}
// user.dto.ts
export class UserDto {
username: string;
password: string;
}
DTO와 Interface의 차이점
DTO : 계층 간 데이터 교환을 위해 사용되는 객체로 주로 데이터베이스에서 데이터를 가져와 클라이언트에게 전송할 때 사용됩니다.
Interface : 객체의 구조를 정의하는 방법 중 하나로 프로퍼티와 메서드의 이름, 타입을 정의할 수 있으며 클래스가 구현해야 하는 계약 또는 구조글을 명시합니다.
차이점 : Interface는 객체의 타입을 정의하는 역할을 하며 주로 컴파일 타임에 타입 체킹을 위해 사용되는 반면 DTO는 런타임에 데이터를 전달하는 데 사용되는 객체로 데이터 전송의 최적화 및 필요한 데이터의 선택적 포함을 위해 사용됩니다.
// user.controller.ts
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { UserService } from './user.service';
import { UserDto } from './dto/user.dto';
import { User } from './user.entity';
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
// 입력 받은 user 정보를 비즈니스 로직으로 보냄
@Post('/signup')
async signUp(@Body() userDto: UserDto): Promise<string> {
return this.userService.signUp(userDto);
}
@Get('/signin')
async singIn(@Query() userDto: UserDto): Promise<string> {
return this.userService.signIn(userDto);
}
@Get('/list')
async userList(): Promise<User[]> {
return this.userService.userList();
}
}
// user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { UserDto } from './dto/user.dto';
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
// 회원가입 비즈니스 로직
async signUp(userDto: UserDto) {
const { username, password } = userDto;
// 가입되어 있는 유저인지 확인하기 위해 username을 조회
const findUser = await this.userRepository.findUser(username);
// 유저 정보가 없을 경우 데이터베이스에 저장
if (findUser) {
throw new Error('user already exists');
} else {
return this.userRepository.signUp(userDto);
}
}
// 로그인
async signIn(userDto: UserDto) {
return this.userRepository.signIn(userDto);
}
// 가입된 유저 리스트
async userList() {
return this.userRepository.userList();
}
}
// user.repository.ts
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { UserDto } from './dto/user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
// 회원가입
async signUp(userDto: UserDto): Promise<string> {
const { username, password } = userDto;
const user = await this.userRepository.create({ username, password });
await this.userRepository.save(user);
return 'signup success';
}
// 로그인
async signIn(userDto: UserDto): Promise<string> {
const { username, password } = userDto;
const userCheck = await this.userRepository.findOneBy({
username,
password,
});
if (userCheck) {
return 'signin success';
} else {
return 'signin fail';
}
}
// 같은 유저명을 가진 유저가 있는지 확인
async findUser(username: string): Promise<User | null> {
return this.userRepository.findOneBy({ username });
}
// 회원가입된 유저 리스트
async userList(): Promise<User[]> {
const users = await this.userRepository.find({
select: ['username'],
});
return users;
}
}
회원가입 요청
로그인 시도
가입된 유저 정보
TypeORM 공식문서상에서 repository 패턴을 사용하는 것으로 보아 이 패턴은 권장되는 것 같은데 어떤 게 정답이다라고 단정 지을 수 없을 거 같습니다.
아직은 파일 간의 의존성 주입이라던지 각 역할의 정의가 애매해 실습하는 동안 우왕좌왕했는데 이번 실습을 계기로 역할 및 의존성에 대해 알게 되어 뜻깊은 실습이 됐습니다.
'Nestjs' 카테고리의 다른 글
[NestJs] 클린 코드 알아보기 (0) | 2024.03.28 |
---|---|
[NestJs] 로그인 기능 구현 (0) | 2024.03.23 |
[NestJs] TypeORM 연결하기 (3) | 2024.03.18 |
[NestJs] Middleware 알아보기 (1) | 2024.03.15 |
[NestJs] Interceptors 알아보기 (0) | 2024.03.14 |