Nest.js(4)
MySQL ์ค์
๐ก ORM์ด๋?
ORM(Object-Relational Mapping)์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ด๊ณ๋ฅผ ๊ฐ์ฒด๋ก ๋ฐ๊พธ์ด ๊ฐ๋ฐ์๊ฐ OOP๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฝ๊ฒ ๋ค๋ฃฐ ์ ์๋๋ก ํด ์ฃผ๋ ๋๊ตฌ์ ๋๋ค. SQL๋ฌธ์ ๊ทธ๋๋ก ์ฝ๋์ ๊ธฐ์ ํ๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ฟผ๋ฆฌ์ ์ผ๋ก ๋ค๋ฃจ๋ ๋ฐฉ์์์ ์ธ๋ถ ์ฟผ๋ฆฌ๋ฌธ์ ์ถ์ํํ๋ ๊ฒ์ผ๋ก ๋ฐ์ ํ์์ต๋๋ค. ๊ฐ๋ฐ์๋ ORM์์ ์ ๊ณตํ๋ ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ผ๋ฐ์ ์ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํธ์ถํ๋ฏ DB์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฐ์ดํธํ๊ณ ์กฐํํ ์ ์์ต๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์
DBeaver์ MySql๋ฅผ ์ด์ฉํด์ ๊ฐ๋ฐ ํ๋ค.
ORM ๊ฐ๋ฅํ ๊ธฐ๋ฅ์ค์ TypeORM ์ ์ฌ์ฉํฉ๋๋ค.
@nestjs/typeorm ํจํค์ง์์ ์ ๊ณตํ๋ TypeOrmModule์ ์ด์ฉํ์ฌ DB์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค.
...
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
...
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'test',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
})
export class AppModule {}
- type: TypeOrmModule์ด ๋ค๋ฃจ๊ณ ์ ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ์ ์ ๋๋ค. ์ฐ๋ฆฌ๋ MySQL์ ์ด์ฉํฉ๋๋ค.
- host: ์ฐ๊ฒฐํ ํ ์ดํฐ๋ฒ ์ด์ค ํธ์คํธ ์ฃผ์ ์ ๋ ฅ
- port: ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐ๊ฒฐ์ ์ํด ์ด์ด๋์ ํฌํธ๋ฒํธ์ ๋๋ค. MySQL์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก 3306๋ฒ ํฌํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- username, password : ๋น๋ฒ ๋ฐ ์์ด๋
- database: ์ฐ๊ฒฐํ๊ณ ์ ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์ด๋ฆ์ ๋๋ค. ์์ ๋ง๋ test ์คํค๋ง๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- entities: ์์ค ์ฝ๋ ๋ด์์ TypeORM์ด ๊ตฌ๋๋ ๋ ์ธ์ํ๋๋ก ํ ์ํฐํฐ ํด๋์ค์ ๊ฒฝ๋ก๋ฅผ ์ง์ ํฉ๋๋ค.
- synchronize: ์๋น์ค ๊ตฌ๋์ ์์ค์ฝ๋ ๊ธฐ๋ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ๋๊ธฐํ ํ ์ง ์ฌ๋ถ์ ๋๋ค. ๋ก์ปฌํ๊ฒฝ์์ ๊ตฌ๋ํ ๋๋ ๊ฐ๋ฐ์ ํธ์๋ฅผ ์ํด true๋ก ํฉ๋๋ค. ( ์์ค ์ฝ๋ ๋ณ๊ฒฝํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ๋ณ๊ฒฝ๋๋ค. )
โ ๏ธ synchronize ์ต์ ์ true๋ก ํ๋ฉด ์๋น์ค๊ฐ ์คํ๋๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ฐ๊ฒฐ๋ ๋ ํญ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ด๊ธฐํ ๋๋ฏ๋ก ์ ๋ ํ๋ก๋์ ์๋ true๋ก ํ์ง ๋ง์ธ์!
TypeOrmModule.forRoot ํจ์์ ์ ๋ฌํ๋ TypeOrmModuleOptions ๊ฐ์ฒด๋ฅผ ์ข ๋ ์์ธํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
export declare type TypeOrmModuleOptions = {
retryAttempts?: number;
retryDelay?: number;
toRetry?: (err: any) => boolean;
autoLoadEntities?: boolean;
keepConnectionAlive?: boolean;
verboseRetryLog?: boolean;
} & Partial<ConnectionOptions>;
- retryAttempts: ์ฐ๊ฒฐ์ ์ฌ์๋ ํ์. ๊ธฐ๋ณธ๊ฐ์ 10์ ๋๋ค.
- retryDelay: ์ฌ์๋ ๊ฐ์ ์ง์ฐ ์๊ฐ. ๋จ์๋ ms์ด๊ณ ๊ธฐ๋ณธ๊ฐ์ 3000์ ๋๋ค.
- toRetry: ์๋ฌ๊ฐ๊ฐ ๋ฌ์ ๋ ์ฐ๊ฒฐ์ ์๋ํ ์ง ํ๋จํ๋ ํจ์. ์ฝ๋ฐฑ์ผ๋ก ๋ฐ์ ์ธ์ err ๋ฅผ ์ด์ฉํ์ฌ ์ฐ๊ฒฐ์ฌ๋ถ๋ฅผ ํ๋จํ๋ ํจ์๋ฅผ ๊ตฌํํ๋ฉด ๋ฉ๋๋ค.
- autoLoadEntities: ์ํฐํฐ๋ฅผ ์๋ ๋ก๋ํ ์ง ์ฌ๋ถ.
- keepConnectionAlive: ์ ํ๋ฆฌ์ผ์ด์ ์ข ๋ฃ ํ ์ฐ๊ฒฐ์ ์ ์งํ ์ง ์ฌ๋ถ.
- verboseRetryLog: ์ฐ๊ฒฐ์ ์ฌ์๋ ํ ๋ verbose ์๋ฌ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ค ์ง ์ฌ๋ถ. ๋ก๊น ์์ verbose ๋ฉ์์ง๋ ์์ธ ๋ฉ์์ง๋ฅผ ์๋ฏธํฉ๋๋ค.
TypeOrmModuleOptions๋ ์์์ ์ค๋ช ํ ํ์ ๊ณผ ConnectionOptions ํ์ ์ Partialํ์ ์ ๊ต์ฐจ(&)ํ ํ์ ์ ๋๋ค. Partial ์ ๋ค๋ฆญ ํ์ ์ ์ ์ธํ ํ์ ์ ์ผ๋ถ ์์ฑ๋ง์ ๊ฐ์ง ์ ์๋๋ก ํ๋ ํ์ ์ ๋๋ค. ๊ต์ฐจ ํ์ ์ ๊ต์ฐจ์ํจ ํ์ ์ ์์ฑ๋ค์ ๋ชจ๋ ๊ฐ์ง๋ ํ์ ์ ๋๋ค. ConnectionOptions์ ์ ์๋ฅผ ๋ณด๋ฉด Nest๊ฐ ์ง์ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ด๋ค ๊ฒ๋ค์ธ์ง ์ ์ ์์ต๋๋ค. MySQL ์ธ์๋ PostgreSQL, MsSQL, Oracle ๋ฑ ๋ฟ ์๋๋ผ Native, Mongo, Amazon Aurora๋ ์ง์ํฉ๋๋ค.
ormconfig.json ํ์ฉ
Nest๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฐ๊ฒฐ๋ ๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ๋ฃจํธ ๋๋ ํ ๋ฆฌ์ ormconfig.jsonํ์ผ์ด ์๋ค๋ฉด TypeOrmModule.forRoot()์ ์ต์ ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ์ง ์์๋ ๋ฉ๋๋ค. ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ด JSONํ์ผ์๋ ์ํฐํฐ์ ๊ฒฝ๋ก๋ฅผ __dirname์ผ๋ก ๋ถ๋ฌ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ ๋น๋ ํ ์์ฑ๋๋ ๋๋ ํ ๋ฆฌ์ด๋ฆ์ธ dist๋ฅผ ๋ถ์ฌ์ฃผ์ด์ผ ํฉ๋๋ค.
- ormconfig.json
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "test",
"database": "test",
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": true
}
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot(),
],
})
export class AppModule {}
ํ์ ๊ฐ์ ์ ์์ฒญํ ์ ์ ์ ์ ๋ณด ์ ์ฅํ๊ธฐ
Nest๋ ๋ฆฌํผ์งํ ๋ฆฌ ํจํด์ ์ง์ํฉ๋๋ค. ๋ฆฌํฌ์งํ ๋ฆฌ๋ ๋ฐ์ดํฐ ์๋ณธ์ ์ก์ธ์คํ๋ ๋ฐ ํ์ํ ๋ ผ๋ฆฌ๋ฅผ ์บก์ํํ๋ ํด๋์ค ๋๋ ๊ตฌ์ฑ ์์์ ๋๋ค. ๋ฆฌํฌ์งํ ๋ฆฌ๋ ๊ณตํต ๋ฐ์ดํฐ ์ก์ธ์ค ๊ธฐ๋ฅ์ ์ง์คํด ๋ ๋์ ์ ์ง๊ด๋ฆฌ๋ฅผ ์ ๊ณตํ๊ณ ๋๋ฉ์ธ ๋ชจ๋ธ ๊ณ์ธต์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ก์ธ์คํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๊ธฐ์ ์ด๋ ์ธํ๋ผ๋ฅผ ๋ถ๋ฆฌํฉ๋๋ค.
- user.entity.ts
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('User')
export class UserEntity {
@PrimaryColumn()
id: string;
@Column({ length: 30 })
name: string;
@Column({ length: 60 })
email: string;
@Column({ length: 30 })
password: string;
@Column({ length: 60 })
signupVerifyToken: string;
}
์๋ฒ๋ฅผ ๊ตฌ๋ํ๋ฉด DB ์ ํ ์ด๋ธ์ด ์์ฑ๋๋ค. ๋๊ธฐํ ์ต์ ์ true ๋ก ํด๋์๊ธฐ ๋๋ฌธ์ด๋ค.
ํธ๋์ญ์ ์ ์ฉ
TypeORM์์ ํธ๋์ญ์ ์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ 3๊ฐ์ง๊ฐ ์์ต๋๋ค.
- QueryRunner๋ฅผ ์ด์ฉํด์ ๋จ์ผ DB ์ปค๋ฅ์ ์ํ๋ฅผ ์์ฑํ๊ณ ๊ด๋ฆฌํ๊ธฐ
- transaction ๊ฐ์ฒด๋ฅผ ์์ฑํด์ ์ด์ฉํ๊ธฐ
- @Transaction, @TransactionManager, @TransactionRepository ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ
QueryRunner ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
QueryRunner๋ฅผ ์ด์ฉํ๋ฉด ํธ๋์ญ์ ์ ์์ ํ ์ ์ดํ ์ ์์ต๋๋ค. Connection ์ฃผ์ ํฉ๋๋ค.
...
import { Connection, ... } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
...
private connection: Connection,
) { }
...
}
์ด์ Connection ๊ฐ์ฒด์์ ํธ๋์ญ์ ์ ์์ฑํ ์ ์์ต๋๋ค. ์ ์ ๋ฅผ ์ ์ฅํ๋ ๋ก์ง์ ํธ๋์ญ์ ์ ๊ฑธ์ด๋ด ์๋ค.
private async saveUserUsingQueryRunner(name: string, email: string, password: string, signupVerifyToken: string) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = new UserEntity();
user.id = ulid();
user.name = name;
user.email = email;
user.password = password;
user.signupVerifyToken = signupVerifyToken;
await queryRunner.manager.save(user);
// throw new InternalServerErrorException(); // ์ผ๋ถ๋ฌ ์๋ฌ๋ฅผ ๋ฐ์์์ผ ๋ณธ๋ค
await queryRunner.commitTransaction();
} catch (e) {
// ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ๋กค๋ฐฑ
await queryRunner.rollbackTransaction();
} finally {
// ์ง์ ์์ฑํ QueryRunner๋ ํด์ ์์ผ ์ฃผ์ด์ผ ํจ
await queryRunner.release();
}
}
- ์ฃผ์ ๋ฐ์ Connection ๊ฐ์ฒด์์ QueryRunner๋ฅผ ์์ฑํฉ๋๋ค.
- QueryRunner์์ DB์ ์ฐ๊ฒฐ ํ ํธ๋์ญ์ ์ ์์ํฉ๋๋ค.
- ์ ์ ๋์์ ์ํํ๋ค๋ฉด ํธ๋์ญ์ ์ ์ปค๋ฐํ์ฌ ์์ํ์ํต๋๋ค.
- DB ์์ ์ ์ํํ ํ ์ปค๋ฐ์ ํด์ ์์ํ๋ฅผ ์๋ฃํฉ๋๋ค.
- ์ด ๊ณผ์ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์ง์ ๋กค๋ฐฑ์ ์ํํฉ๋๋ค.
- finally๊ตฌ๋ฌธ์ ํตํด ์์ฑํ QueryRunner ๊ฐ์ฒด๋ฅผ ํด์ ํฉ๋๋ค. ์์ฑํ QueryRunner๋ ํด์ ์์ผ ์ฃผ์ด์ผ ํฉ๋๋ค.
transaction ๊ฐ์ฒด๋ฅผ ์์ฑํด์ ์ด์ฉํ๋ ๋ฐฉ๋ฒ
transaction ๋ฉ์๋๋ EntityManager๋ฅผ ์ฝ๋ฐฑ์ผ๋ก ๋ฐ์ ์ฌ์ฉ์๊ฐ ์ด๋ค ์์ ์ ์ํํ ํจ์๋ฅผ ์์ฑํ ์ ์๋๋ก ํด ์ค๋๋ค.
private async saveUserUsingTransaction(name: string, email: string, password: string, signupVerifyToken: string) {
await this.connection.transaction(async manager => {
const user = new UserEntity();
user.id = ulid();
user.name = name;
user.email = email;
user.password = password;
user.signupVerifyToken = signupVerifyToken;
await manager.save(user);
// throw new InternalServerErrorException();
})
}
@Transaction, @TransactionManager, @TransactionRepository ์ด์ฉ
โฉโฉ
๋ง์ด๊ทธ๋ ์ด์
๋ง์ฝ ์ด์ ์ ์ ์ฅํด ๋ ๋ฐ์ดํฐ๊ฐ ํ์ฌ์ ๋๋ฉ์ธ ๊ตฌ์กฐ์ ๋ค๋ฅด๋ค๋ฉด ๋ชจ๋ ๋ฐ์ดํฐ์ ๊ฐ์ ์์ ํ ์ผ๋ ์๊น๋๋ค. ์ด๋ฐ ๊ณผ์ ์ญ์ ๋ง์ด๊ทธ๋ ์ด์ ์ด๋ผ๊ณ ๋ถ๋ฆ ๋๋ค. ์ฝ๊ฒ ์ด์ผ๊ธฐ ํ๋ฉด ํ์ฌ ์์ฑ๋ DB ์คํค๋ง์ ์ค์ DB ์คํค๋ง๋ฅผ syncํด์ฃผ๋ ๊ฒ์ด๋ค
- ์ฒซ์งธ ๋ง์ด๊ทธ๋ ์ด์ ์ ์ํ SQL๋ฌธ์ ์ง์ ์์ฑํ์ง ์์๋ ๋ฉ๋๋ค.
- ๋ง์ฝ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์๋ชป ์ ์ฉ๋์๋ค๋ฉด ๋ง์ง๋ง ์ ์ฉํ ๋ง์ด๊ทธ๋ ์ด์ ์ฌํญ์ ๋๋๋ฆฌ๋ ์์ ๋ ๊ฐ๋จํ ๋ช ๋ น์ด๋ก ์ํํ ์ ์์ต๋๋ค.
- ๋ฌผ๋ก ๋ฐ์ดํฐ์ ๊ฐ์ ๋ณ๊ฒฝํ๋ ๋ง์ด๊ทธ๋ ์ด์ ์ด๋ผ๋ฉด ์๋ณตํ๋ ์ฝ๋๋ฅผ ์ง์ ์์ฑํด์ผ ํ๊ธฐ๋ ํฉ๋๋ค.
- ๋กค๋ง ์ ๋ฐ์ดํธ๊ฐ ๊ฐ๋ฅํ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์๋๋ผ๋ฉด ์ ์ฉํ๊ธฐ ์ ์ DB๋ฅผ ๋ฐฑ์ ํ๋ ๊ฒ์ ์์ง ๋ง์์ผ ํฉ๋๋ค
- TypeORM ๋ง์ด๊ทธ๋ ์ด์ ์ ์ด์ฉํ๋ฉด ๋ง์ด๊ทธ๋ ์ด์ ์ฝ๋๋ฅผ ์ผ์ ํ ํ์์ผ๋ก ์์ค ์ ์ฅ์์์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค. ์ฆ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ณ๊ฒฝ์ ์ ์์ค ์ฝ๋๋ก ๊ด๋ฆฌํ ์ ์๋ค๋ ๋ป์ ๋๋ค
- TypeORM ๋ง์ด๊ทธ๋ ์ด์ ์ ์ ์ฉํ์ฌ ๋ง์ด๊ทธ๋ ์ด์ ์ด๋ ฅ์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค. ์ธ์ ์ด๋ค ๋ง์ด๊ทธ๋ ์ด์ ์ด ์ผ์ด๋ฌ๋์ง๋ฅผ ํน์ ํ ์ด๋ธ์ ๊ธฐ๋กํ๊ณ , ํ์ํ ๊ฒฝ์ฐ ์ฒ์๋ถํฐ ์์๋๋ก ๋ค์ ์ํํ ์๋ ์์ต๋๋ค.
๋ฏธ๋ค์จ์ด
Nest์ ๋ฏธ๋ค์จ์ด๋ ๊ธฐ๋ณธ์ ์ผ๋ก Express์ ๋ฏธ๋ค์จ์ด์ ๋์ผํฉ๋๋ค. Express ๋ฌธ์์๋ ๋ฏธ๋ค์จ์ด๊ฐ ๋ค์๊ณผ ๊ฐ์ ๋์์ ์ํํ ์ ์๋ค๊ณ ๊ธฐ์ ๋์ด ์์ต๋๋ค.
- ์ด๋ค ํํ์ ์ฝ๋๋ผ๋ ์ํํ ์ ์์ต๋๋ค.
- ์์ฒญ๊ณผ ์๋ต์ ๋ณํ์ ๊ฐํ ์ ์์ต๋๋ค.
- ์์ฒญ-์๋ต ์ฃผ๊ธฐ๋ฅผ ๋๋ผ ์ ์์ต๋๋ค.
- ์ฌ๋ฌ ๊ฐ์ ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ค๋ฉด next()๋ก ํธ์ถ ์คํ์ ๋ค์ ๋ฏธ๋ค์จ์ด์๊ฒ ์ ์ด๊ถ์ ์ ๋ฌํฉ๋๋ค.
๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉํ์ฌ ๋ค์๊ณผ ๊ฐ์ ์์ ๋ค์ ์ํํฉ๋๋ค.
- ์ฟ ํค ํ์ฑ: ์ฟ ํค๋ฅผ ํ์ฑํ์ฌ ์ฌ์ฉํ๊ธฐ ์ฌ์ด ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํฉ๋๋ค. ์ด๋ฅผ ์ด์ฉํ๋ฉด ๋ผ์ฐํฐ ํธ๋ค๋ฌ๊ฐ ๋งค๋ฒ ์ฟ ํค๋ฅผ ํ์ฑํ ํ์๊ฐ ์์ต๋๋ค.
- ์ธ์ ๊ด๋ฆฌ: ์ธ์ ์ฟ ํค๋ฅผ ์ฐพ๊ณ , ํด๋น ์ฟ ํค์ ๋ํ ์ธ์ ์ ์ํ๋ฅผ ์กฐํํด์ ์์ฒญ์ ์ธ์ ์ ๋ณด๋ฅผ ์ถ๊ฐํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ค๋ฅธ ํธ๋ค๋ฌ๊ฐ ์ธ์ ๊ฐ์ฒด๋ฅผ ์ด์ฉํ ์ ์๋๋ก ํด ์ค๋๋ค.
- ์ธ์ฆ/์ธ๊ฐ: ์ฌ์ฉ์๊ฐ ์๋น์ค์ ์ ๊ทผ ๊ฐ๋ฅํ ๊ถํ์ด ์๋์ง ํ์ธํฉ๋๋ค. ๋จ, Nest๋ ์ธ๊ฐ๋ฅผ ๊ตฌํํ ๋ ๊ฐ๋(Guard)๋ฅผ ์ด์ฉํ๋๋ก ๊ถ์ฅํ๊ณ ์์ต๋๋ค.
- ๋ณธ๋ฌธ(body) ํ์ฑ: ๋ณธ๋ฌธ์ POST/PUT ์์ฒญ์ผ๋ก ๋ค์ด์ค๋ json ํ์ ๋ฟ ์๋๋ผ ํ์ผ ์คํธ๋ฆผ๊ณผ ๊ฐ์ ๋ฐ์ดํฐ๋ ์์ต๋๋ค. ์ด ๋ฐ์ดํฐ๋ฅผ ์ ํ์ ๋ฐ๋ผ ์ฝ๊ณ ํด์ํ ๋ค์ ํ๋ผ๋ฏธํฐ์ ๋ฃ๋ ์์ ์ ํฉ๋๋ค. ์์ ์ปจํธ๋กค๋ฌ ์ฅ์์ ๋ณด์๋ ๋ณธ๋ฌธ์ ์ด๋ ๊ฒ ๋ถ์๋ ๊ฒฐ๊ณผ๊ฐ ํฌํจ๋์ด ์์ต๋๋ค.
๋ฏธ๋ค์จ์ด๋ ํจ์๋ก ์์ฑํ๊ฑฐ๋ NestMiddleware ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ํด๋์ค๋ก ์์ฑํ ์ ์์ต๋๋ค. ๋ค์ด์จ ์์ฒญ์ ํฌํจ๋ ์ ๋ณด๋ฅผ ๋ก๊น ํ๊ธฐ ์ํ Logger๋ฅผ ๋ฏธ๋ค์จ์ด๋ก ๊ตฌํํด ๋ด ์๋ค.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
๋ฏธ๋ค์จ์ด๋ฅผ ๋ชจ๋์ ํฌํจ์ํค๊ธฐ ์ํด์๋ ํด๋น ๋ชจ๋์ NestModule ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ผ ํฉ๋๋ค. NestModule์ ์ ์ธ๋ configureํจ์๋ฅผ ํตํด ๋ฏธ๋ค์จ์ด๋ฅผ ์ค์ ํฉ๋๋ค.
- logger.middleware.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './logger/logger.middleware';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any {
consumer
.apply(LoggerMiddleware)
.forRoutes('/users');
}
}
/users ๊ฒฝ๋ก๋ก ๋ค์ด์ค๋ ์์ฒญ์ ์ํํด ๋ณด๋ฉด ์ฝ์์ Request... ์ด ์ฐํ๋ ๊ฑธ ํ์ธํ ์ ์์ต๋๋ค.
MiddlewareConsumer
์ด์ ์ฝ๋์์ confiure ๋ฉ์๋์ ์ธ์๋ก ์ ๋ฌ๋ MiddlewareConsumer ๊ฐ์ฒด๋ฅผ ์ด์ฉํด์ ๋ฏธ๋ค์จ์ด๋ฅผ ์ด๋์ ์ ์ฉํ ์ง ๊ด๋ฆฌํ ์ ์์ต๋๋ค. apply ๋ฉ์๋์ ์ํ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
excludeํจ์๋ ์์ํ๋ฏ์ด ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉํ์ง ์์ ๋ผ์ฐํ ๊ฒฝ๋ก๋ฅผ ์ค์ ํฉ๋๋ค.
...
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any {
consumer
.apply(LoggerMiddleware, Logger2Middleware)
.exclude({ path: 'users', method: RequestMethod.GET },)
.forRoutes(UsersController)
}
}
์ ์ญ์ผ๋ก ์ ์ฉํ๊ธฐ
ํจ์๋ก ์ ์๋ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ค์ ๋ง๋ค์ด ๋ด ์๋ค.
import { Request, Response, NextFunction } from 'express';
export function logger3(req: Request, res: Response, next: NextFunction) {
console.log(`Request3...`);
next();
};
๊ทธ๋ฆฌ๊ณ main.ts์์ ์ ์ฉํฉ๋๋ค.
import { logger3 } from './logger3/logger3.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(logger3);
await app.listen(3000);
}
bootstrap();
exclude ์ต์ ์ ๋ค์ ํ๊ณ ์์ฒญ์ ๋ณด๋ด๋ฉด logger3 ๋ฏธ๋ค์จ์ด๊ฐ ๋จผ์ ์ ์ฉ๋๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๐ก ํจ์๋ก ๋ง๋ ๋ฏธ๋ค์จ์ด์ ๋จ์ ์ DI ์ปจํ ์ด๋๋ฅผ ์ฌ์ฉํ ์ ์๋ค๋ ๊ฒ์ ๋๋ค. ์ฆ, ํ๋ก๋ฐ์ด๋๋ฅผ ์ฃผ์ ๋ฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
JWT ( Guard )
- ์ธ์ฆ์ ์์ฒญ์๊ฐ ์์ ์ด ๋๊ตฌ์ธ์ง ์ฆ๋ช ํ๋ ๊ณผ์ ์ ๋๋ค.
- ์ธ๊ฐ(Authorization)๋ ์ธ์ฆ์ ํต๊ณผํ ์ ์ ๊ฐ ์์ฒญํ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ๊ถํ์ด ์๋์ง๋ฅผ ํ๋ณํ๋ ๊ฒ์ ๋งํฉ๋๋ค
๋ฏธ๋ค์จ์ด๋ ์คํ ์ปจํ ์คํธ(ExecutionContext)์ ์ ๊ทผํ์ง ๋ชปํฉ๋๋ค. ๊ฐ๋๋ ์คํ ์ปจํ ์คํธ ์ธ์คํด์ค์ ์ ๊ทผํ ์ ์์ด ๋ค์ ์คํ๋ ์์ ์ ์ ํํ ์๊ณ ์์ต๋๋ค.
๊ฐ๋๋ฅผ ์ด์ฉํ ์ธ๊ฐ
๊ฐ๋๋ CanActivate ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ผ ํฉ๋๋ค.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: any) {
return true;
}
}
์คํ ์ปจํ ์คํธ
canActivate ํจ์๋ ExecutionContext ์ธ์คํด์ค๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค. ExecutionContext๋ ArgumentsHost๋ฅผ ์์๋ฐ๋๋ฐ, ์์ฒญ๊ณผ ์๋ต์ ๋ํ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ฐ๋ฆฌ๋ http๋ก ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ ์์ผ๋ฏ๋ก ์ธํฐํ์ด์ค์์ ์ ๊ณตํ๋ ํจ์ ์ค switchToHttp() ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ํ์ํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
๊ฐ๋ ์ ์ฉ
๊ฐ๋๋ฅผ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ์์ ์์ธ ํํฐ๋ฅผ ์ ์ฉํ๋ ๊ฒ๊ณผ ์ ์ฌํฉ๋๋ค. ์ปจํธ๋กค๋ฌ ๋ฒ์ ๋๋ ๋ฉ์๋ ๋ฒ์๋ก ์ ์ฉํ๊ณ ์ ํ๋ค๋ฉด @UseGuards(AuthGuard) ์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค. AuthGuard ์ธ์คํด์ค์ ์์ฑ์ Nest๊ฐ ๋งก์์ ํฉ๋๋ค. ๋ง์ฝ ์ฌ๋ฌ ์ข ๋ฅ์ ๊ฐ๋๋ฅผ ์ ์ฉํ๊ณ ์ถ๋ค๋ฉด ์ผํ๋ก ์ด์ด ์ ์ธํ๋ฉด ๋ฉ๋๋ค.
@UseGuards(AuthGuard)
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
@UseGuards(AuthGuard)
@Get()
getHello(): string {
return this.appService.getHello();
}
}
๊ฐ๋๋ฅผ ์ด์ฉํ ์ธ๊ฐ ์ฒ๋ฆฌ
์ปจํธ๋กค๋ฌ์ “์ ์ ์ ๋ณด ์กฐํ” ๊ตฌํ์ ๋ค์ ์ดํด๋ด ์๋ค.
@Get(':id')
async getUserInfo(@Headers() headers: any, @Param('id') userId: string): Promise<UserInfo> {
const jwtString = headers.authorization.split('Bearer ')[1];
this.authService.verify(jwtString);
return this.usersService.getUserInfo(userId);
}
ํ์ฌ ๊ตฌํ๋ฐฉ์์ ํค๋์ ํฌํจ๋ JWT ํ ํฐ์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ ๋ก์ง์ ๋ชจ๋ ์๋ํฌ์ธํธ์ ์ค๋ณต ๊ตฌํํด์ผ ํฉ๋๋ค. ์ด๋ ๋งค์ฐ ๋นํจ์จ์ ์ด๊ณ DRY ์์น์๋ ์๋ฐฐ๋ฉ๋๋ค. ์ฐ๋ฆฌ๋ Nest์์ ์ ๊ณตํ๋ ๊ฐ๋๋ฅผ ์ด์ฉํ์ฌ ์ด๋ฅผ ํธ๋ค๋ฌ ์ฝ๋์์ ๋ถ๋ฆฌํด ๋ด ์๋ค.
์ผ๋จ ์์ ์์๋ก ๋ง๋ ๊ฒ๊ณผ ๊ฐ์ด AuthGuard๋ฅผ ์ ์ฉํด ๋ณด๊ฒ ์ต๋๋ค.
import { Request } from 'express';
import { Observable } from 'rxjs';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: Request) {
const jwtString = request.headers.authorization.split('Bearer ')[1];
this.authService.verify(jwtString);
return true;
}
}
validateRequest ๋ฉ์๋์ ์์ ์ปจํธ๋กค๋ฌ์ ๊ตฌํ๋ ๋ก์ง์ ์ฎ๊ฒจ์์ต๋๋ค. ๊ฐ๋๋ ์ค๋น๋์์ผ๋ AuthGuard๋ฅผ ์ ์ฉํด์ผ ํฉ๋๋ค. ์ ์ญ์ผ๋ก ์ ์ฉํ๊ฒ ๋๋ฉด ํ์๊ฐ์ , ๋ก๊ทธ์ธ ๋ฑ ์ก์ธ์ค ํ ํฐ ์์ด ์์ฒญํ๋ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ผ๋ฏ๋ก ํ์ ์กฐํ ์๋ํฌ์ธํธ์๋ง ์ ์ฉํ๊ฒ ์ต๋๋ค. ๋ฌผ๋ก ์ปจํธ๋กค๋ฌ๋ฅผ ๋ถ๋ฆฌ์ํค๊ณ , ๋ถ๋ฆฌ๋ ์ปจํธ๋กค๋ฌ์ ์ ์ฉํด๋ ๋ฉ๋๋ค.