Entity(Model) 생성 및 Listener 활용
Entities 정의
앞서 DB 구성에서 entities 경로를 다음과 같이 잡아 주었다.
entities: [
"./entities/**/*.*"
]
해당 경로에 entity (Model)을 정의해주도록하자.
github.com/typeorm/typeorm#step-by-step-guide
github 문서의 step-by-step에서는 entity를 생성하는 방식에 대해서 차근차근 알려주고 있다.
@Entity()를 통해 entity를 생성할 수 있고
@Column을 통해 열을 정의할 수 있으며 추가적으로 타입을 지정할 수도 있다.
@PrimaryGeneratedColumn() 나 @PrimaryColumn()와 같이 key를 지정하는 데코레이션도 있음을 확인할 수 있다.
좀 더 자세한 사항은 공식문서에서 확인할 수 있다. 사용하는 DB에 따라 지정할 수 있는 타입을 종류나 @Tree와 같은 새로운 데코레이터도 안내되어 있으니 필요할 때 참고하면 될 것이다.
typeorm.io/#/entities/column-types-for-postgres
이를 이용해서 User 항목의 entity를 만들어보자. 조금 verbose한 편인데, 그래도 전반적인 entity를 정의하는데 있어서 필요한 것들을 살펴볼 수 있긴 하다.
추가적으로 알아둬야 할 사항은
typescript 기반 객체 지향 프로그래밍의 국룰인 class-validator를 이용하였고 (typeORM과 관련 없지만)
class 내부 getter를 사용했으며 double precision이라는 데이터타입을 사용했다는 것이다.
import { IsEmail } from "class-validator";
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn
} from "typeorm";
@Entity()
class User extends BaseEntity {
@PrimaryGeneratedColumn() id: number;
@Column({ type: "text", unique: true })
@IsEmail()
email: string;
@Column({ type: "boolean", default: false })
verifiedEmail: boolean;
@Column({ type: "text" })
firstName: string;
@Column({ type: "text" })
lastName: string;
@Column({ type: "int" })
age: number;
@Column({ type: "text" })
password: string;
@Column({ type: "text" })
phoneNumber: string;
@Column({ type: "boolean", default: false })
verifiedPhonenNumber: boolean;
@Column({ type: "text" })
profilePhoto: string;
@Column({ type: "boolean", default: false })
isDriving: boolean;
@Column({ type: "boolean", default: false })
isRiding: boolean;
@Column({ type: "boolean", default: false })
isTaken: boolean;
@Column({ type: "double precision", default: 0 })
lastLng: number;
@Column({ type: "double precision", default: 0 })
lastLat: number;
@Column({ type: "double precision", default: 0 })
lastOrientation: number;
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
@CreateDateColumn() createdAt: string;
@UpdateDateColumn() updatedAt: string;
}
export default User;
Listener
기타 Listener와 Subscriber에 대한 내용은 다음 문서를 참고하자.
github.com/typeorm/typeorm/blob/master/docs/listeners-and-subscribers.md
특정 이벤트에 반응하기 위한 리스너이다. 종류도 몇개 없고, 데코레이션 명칭도 명확해서 어떤 동작을 캐치할 수 있는지 쉽게 알 수 있다.
위에 작성한 entity에 password 암호화를 위해 listener를 활용해보자.
우선 password 암호화를 위해 bcrypt 패키지를 설치해보자
yarn add bcrypt
yarn add @types/bcrypt --dev
플레인한 비밀번호를 받아 hash + salt 화를 진행한다. (bcrypt를 활용한 해쉬, 솔트) 관련 내용은 전에 작성한 포스트에서 확인할 수 있다. 참고로 첨부한 포스트에서는 salt를 생성하는 함수를 별도로 만들었지만 아래는 상수를 넣어서 간단히 처리했다.
암호화를 위해 Listener인 @BeforeInsert()와 @BeforeUpdate()를 사용하였다.
import { IsEmail } from "class-validator";
import bcrypt from "bcrypt";
import {
BaseEntity,
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn
} from "typeorm";
const BCRYPT_ROUNDS = 10;
@Entity()
class User extends BaseEntity {
@PrimaryGeneratedColumn() id: number;
... 중략
private hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, BCRYPT_ROUNDS)
}
@BeforeInsert()
@BeforeUpdate()
async savePassword(): Promise<void> {
if (this.password) {
const hashedPassword = await this.hashPassword(this.password)
this.password = hashedPassword
}
}
}
export default User;
* cautions!
User entity에는 @BeforeUpdate() 리스너를 달아뒀는데 아래와 같은 update로는 해당 리스너가 트리거되지 않습니다.
await User.update({ id: user.id }, { ...notNull });
왜냐하면 리스너는 인스턴스가 업데이트 될 때 반영되는 것이지, 엔티티 메서드를 사용한다고 해서 반영되는 것이 아니기 때문입니다. 다시 말해 아래와 같이 User 엔티티의 인스턴스를 변화시키는 꼴에서야 반응한다는 것입니다.
const user = User.findone({id});
user.password = 1;
user.save();
만약 @BeforeUpdate 부분에 비밀번호 해쉬화와 같은 중요한 로직을 달아뒀다면 문제가 되겠지요. 따라서 별도의 로직을 만들어서 처리해야 합니다. 이 부분은 프로젝트마다 다를 것이므로 각자 특성에 맞게 수정해야 합니다.
음... 추후 참고를 위해 일단 현재 작성한 방법을 남겨놓습니다.
const user: User = context.req.user;
const notNull: InotNull = {};
Object.keys(args).forEach((key) => {
if (args[key] !== null) {
notNull[key] = args[key];
}
});
// @BeforeUpdate 리스너 사용을 위해 인스턴스 업데이트를 하기 위함
if (notNull.password !== null && args.password !== null) {
user.password = args.password;
user.save(); // 인스턴스를 조작하였으므로 @BeforeUpdate가 트리거 됨
delete notNull.password; // 평문 password를 저장하면 안되므로 notNull 객체에서 지워주자
}