Entity와 Entity인스턴스 이용시 내부 기능
Intro
시작
유저 도메인을 다시 한번 살펴봅시다.
- 이메일 패스워드를 요청 데이터를 받습니다. 이때
ValidationPipe
를 통해 전달받은 Dto 인스턴스의 유효성을 검사합니다. - 유효하면
UserController
로 전달됩니다. 그후 비즈니스 로직을 수행하기 위해UserService
에게 요청 데이터를 인자로 보내 호출합니다. UserService
는 최종적으로UserRepository
를 호출해DTO
인스턴스를 이용해SQLite DB
에 데이터를 저장합니다.- 역순으로 돌아가 사용자에게 응답 메시지를 보냅니다.
데이터를 저장할 UserRepository
코드는 다음과 같습니다.
@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {
// User 엔티티에 대한 Repository를 주입받아서 repo 변수에 할당하는 생성자 함수입니다.
}
create(email: string, password: string) {
// user 인스턴스를 새로 생성합니다. 만일 user.entity 안에 IsString 등 유효성 메타데이터가 있다면
// 인스턴스 생성시 유효성 검사를 수행합니다.
const user = this.repo.create({email, password})
// user 인스턴스를 저장합니다.
return this.repo.save(user);
// 인스턴스를 사용하지 않고 Dto 자체를 저장할 수도 있습니다.
// return this.repo.save({...user});
}
}
후크로 로그 출력하기
데이터 저장/수정/삭제시 일종의 트리거 작동으로 로그를 기록할 수 있습니다.
AfterInsert
를 이용합니다.
User
엔티티를 기준으로 저장/수정/삭제 시 로그가 출력되도록 entity
파일을 수정합니다.
// user.entity.ts
import { AfterInsert, AfterRemove, AfterUpdate, Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
// 사용자 속성
// PK
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
password: string;
/* Hook */
// Insert 시점에 작동
@AfterInsert()
logInsert() {
console.log('Inserted User with this.id', this.id)
}
// Update 시점에 작동
@AfterUpdate()
logUopdate() {
console.log('Updated User with id', this.id)
}
// Delete 시점에 작동
@AfterRemove()
logRemove() {
console.log('Removed User with id', this.id)
}
}
다만 이때 주의할점이 있는데, 데이터 저장/수정/삭제시 반드시 user 인스턴스를 생성하여 저장/수정/삭제 해주어야 한다는 점입니다. DTO 객체를 이용해 저장/수정/삭제 하려고 하면 후크는 동작하지 않습니다.
@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {
// User 엔티티에 대한 Repository를 주입받아서 repo 변수에 할당하는 생성자 함수입니다.
}
create(email: string, password: string) {
// user 인스턴스를 새로 생성합니다. 만일 user.entity 안에 IsString 등 유효성 메타데이터가 있다면
// 인스턴스 생성시 유효성 검사를 수행합니다.
const user = this.repo.create({email, password})
// 후크가 인식해 작동한다.
return this.repo.save(user);
// 후크가 작동하지 않음.
return this.repo.save({...user});
}
}
동일하게 user 인스턴스를 가져와 일부 정보만 수정해봅시다.
async update(id: number, attrs: Partial<User>) {
// Partial<User>은 해당 클래스의 필드 무엇이든 받을 수 있는 인자이다. 해당 클래스에 존재하지 않는 필드를 인자로 넘길 경우 에러 발생
// 유저 인스턴스를 가져옵니다.
const user = await this.findOne(id);
if(!user) {
throw new Error('user not found');
}
// user 인스턴스에 변경된 부분만을 붙여넣어 재정의합니다.
Object.assign(user, attrs);
return this.repo.save(user);
}
이어서 삭제 메서드입니다.
async remove(id: number) {
const user = await this.findOne(id);
if(!user) {
throw new Error('user not found');
}
return this.repo.remove(user);
}
공통점을 보자면 후크를 사용할 경우 최소 데이터베이스에 2번의 접근이 필요하다는 점입니다. 만일 후크를 사용하지 않고 데이터베이스에 1번의 접근만을 수행할 것이라면 객체로 수정/삭제 해야합니다. 다만 수정/삭제 이전에 실제로 해당 정보가 존재해야 하는지 유무를 검사해야 하기 때문에 2번의 접근이 일어나는 것은 지극히 정상적이라고 볼 수 있습니다.
TypeORM
말고도 다른 ORM(MikroORM 등등..
) 또한 동일한 기능이 대부분 내제되어 있습니다. MikroORM의 경우 AfterInsert
대신 AfterCreate
가 존재하죠. 때문에 다른 ORM을 사용할지라도 대부분의 컨셉은 비슷하니 변경점만 확인해 적용하면 됩니다.
UsersService
의 update
를 사용하기 적합한 컨트롤러와 요청 DTO를 이어서 생성합니다.
요청 DTO입니다.
// update-user.dto.ts
import { IsEmail, IsOptional, IsString } from "class-validator";
export class UpdateUserDto {
@IsEmail()
@IsOptional()
email: string;
@IsString()
@IsOptional()
password: string;
}
컨트롤러 입니다.
// users.controller.ts
@Patch('/:id')
updateUser(@Param('id') id: string, @Body() body: UpdateUserDto) {
return this.usersService.update(parseInt(id), body);
}
예외에 따른 에러 개선하기
서비스 단에서 수정/삭제할 유저가 존재하지 않는 경우 new Error('not found user')
와 같이 에러를 발생시키고 있죠. 단순히 자바스크립트 에러일뿐입니다. 현재 HTTP
프로토콜을 이용하기 때문에 이는 적합한 에러 처리가 아닙니다.
다음과 같이 변경해줍니다.
async update(id: number, attrs: Partial<User>) {
// Partial<User>은 해당 클래스의 필드 무엇이든 받을 수 있는 인자이다. 해당 클래스에 존재하지 않는 필드를 인자로 넘길 경우 에러 발생
// 유저 인스턴스를 가져옵니다.
const user = await this.findOne(id);
if(!user) {
throw new NotFoundException();
}
// user 인스턴스에 변경된 부분만을 붙여넣어 재정의합니다.
Object.assign(user, attrs);
return this.repo.save(user);
}
async remove(id: number) {
const user = await this.findOne(id);
if(!user) {
throw new NotFoundException();
}
return this.repo.remove(user);
}
NotFoundException
의 경우 HTTP프로토콜에 부합하는 예외 케이스 입니다. nestjs/common
패키지내 존재하죠. HTTP 프로토콜 외에도 Websoket
, gRPC
프로토콜이 존재할 수 있습니다. 각 프로토콜 통신마다 적합한 예외 케이스를 적용해야 하니 유의해야 합니다.