(최종 프로젝트 진행중)
오늘 한 일:
깃 cherry picking 방법을 배워서 원격 main에 잘못 push한 실수를 바로잡았다.
내 파트 중 기본 기능 ‘품앗이 CRUD’를 커밋하였다.
팀의 커밋 방식과 PR - .. - pull 사이클을 (수많은 대화 끝에) 조금 수정했다. 이제는 정말 기본 설정을 거의 마쳐서, 냅둬도 굴러가게 될 것 같다.
깃과 씨름하고 API를 실험 작동하다보니 테스트코드가 너무나 필요해졌다. 작성에 뛰어들었다.
(여기 보던 중: Nest.js module-ref)
(그 전에 보던 중: Nest.js testing#end-to-end-testing)
※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※
[GitHub] main에 잘못 ‘push’한 커밋 수습하기
[GitHub] main에 잘못 ‘push’한 커밋 수습하기
A라는 최신 커밋이 있는 main에 내 B 커밋을 잘못 push함. (A - B)
⇒ revert로 되돌림 ⇒ (A - B - A)
⇒ 타 브랜치로 가 B라는 커밋 하나를 찾아서 해시 복사, (로컬) A - B - A 상태인 복사본 브랜치에 돌아와서 git cherry-pick <B 커밋 해시>
⇒ A - B - A - B
⇒ 이제 제대로 원격 feature 브랜치에 push 하고 main으로 pull request를 보내는 절차를 밟으면 된다
배운점:
- main으로 push를 바로 보내는 실수를 반복하지 말아야겠다.
- 다음부터는 꼭 git push origin <로컬과 똑같은 이름의 브랜치>로 push해야겠다.
- 내 이후로 잠시동안 아무도 push나 pull을 하지 않을 상황이라면, 얼른 reset --hard 후에 git push -f origin main 해도 되겠다. (다만 -f이 꺼림칙해서 이번엔 사용하지 않았다)
[TypeORM] Migration
[TypeORM] Migration
TypeORM config 설정 예시:
// ormconfig.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import './src/env';
const ormconfig: TypeOrmModuleOptions = {
type: 'postgres',
username: process.env.RDS_USERNAME,
password: process.env.RDS_PASSWORD,
host: process.env.RDS_HOSTNAME,
port: +process.env.RDS_PORT,
database: process.env.RDS_DB_NAME,
ssl: false,
synchronize: false,
logging: process.env.NODE_ENV === 'dev',
keepConnectionAlive: true,
namingStrategy: new SnakeNamingStrategy(),
entities: [__dirname + '/**/*.entity.{js,ts}'],
subscribers: ['src/subscriber/**/*.ts'],
migrations: [__dirname + '/database/migrations/**/*.ts'],
migrationsTableName: 'migrations',
cli: {
entitiesDir: __dirname + '/**/*.entity.{js,ts}',
migrationsDir: __dirname + '/database/migrations/',
subscribersDir: 'src/subscriber',
},
};
export default ormconfig;
cli.migrationDir 속성은 생성될 migration 파일들이 생겨날 디렉토리이다.
export default로 해야 'typeorm migrate:create' 을 실행할 때 cli.migrationDir 속성이 적용된다.
- ?
(이어지는 설정 및 단계 생략)
기본적으로 Sequealize에서 썼던 방식과 동일한 것 같다.
마이그레이션 방식을 굳이 쓰는 이유:
이렇게 마이그레이션 파일과 typeorm cli를 사용하면 테이블에 변화 과정을 코드로 시간 순서대로 볼 수도있고, 언제든지 Revert할 수 있다는 장점이 있으므로.
나중에 synchronize: true로 인한 팀원간 어긋남이 계속될 때 생각해 볼 수 있는 옵션으로 남겨두자.
(참고: https://2donny-world.tistory.com/23)
사실 알고 싶던 것은 여러 팀원, 그리고 여러 git 버전이 있는 상황에서 한 곳의 config에서 synchronize: false로 해놓는다고 팀원 모두가 공유하고 있는 DB 테이블 상황을 고정시킬 수 있다는 게 사실인지였다.
[TypeORM] Synchronization
[TypeORM] Synchronization
Synchronize makes the entity sync with the database every time you run your application. Hence, whenever you add columns to an entity, create a new table by creating a new entity, or remove columns from an existing table by modifying an entity it will automatically update the database once the server is started.
⇒ 프로덕션 단계의 서비스에서는 위험하다는 단점이 있다. Synchronization보다 migration 매커니즘을 사용하는 게 더 안전하므로 대부분의 상용화된 서비스에서 추천된다.
(참고 - TypORM Synchronization VS Migration 간단한 비교 및 사용법 https://medium.com/swlh/migrations-over-synchronize-in-typeorm-2c66bc008e74#:~:text=Synchronize makes the entity sync,once the server is started.)
[Nest.js] Injection scopes, 주입 생애주기
[Nest.js] Injection scopes, 주입 생애주기
provider scope
: provider의 생애주기.
- DEFAULT는 singleton scope. 애플리케이션과 시작과 끝을 같이하는 것. REQUEST(HTTP 요청별)와 TRNASIENT(컨트롤러별)을 합친 개념. 모든 요청과 컨트롤러가 providers의 한 인스턴스들을 공유한다.
- REQUEST는 (HTTP) request가 들어올 때마다 provider의 새 인스턴스가 생성되는 것. 일단 request가 처리되고 나면 생성된 인스턴스는 garbage-collected된다.
- TRANSIENT는 각 consumer(=컨트롤러)마다 주입받는 provider의 새 인스턴스를 제공받는 것.
Usage, 생애주기(scope) 지정하기
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
참고로 Websocket Gateway들은 반드시 provider들을 singleton 타입으로 써야만 한다고 한다. (request-scoped 불가능). Passport 정책에도.
Controller scope, 컨트롤러의 생애주기(scope) provider의 경우와 타입이 똑같다. (DEFAULT, REQUEST, TRANSIENT)
@Controller({
path: 'cats',
scope: Scope.REQUEST, // 이 컨트롤러는 request-scoped이다.
})
export class CatsController {}
Scope hierarchy
DEFAULT, REQUEST, TRANSIENT 타입 중 REQUEST는 부모 계층으로 타입이 전파된다(bubbles up the injection chain). 즉, 하나라도 request-scoped provider를 가진 controller는 본인도 request-scoped가 된다. 즉 HTTP 요청이 올 때마다 독자적인 새 인스턴스(provider와 controller 모두)를 초기화 한다는 것.
TRANSIENT의 경우는… 잘 모르겠다.
Request provider
HTTP 통신을 하는 서비스 애플리케이션에서는 ‘(날 것 그대로의)원래의 요청 객체’를 사용하고 싶을 수도 있다(express나 fastify가 제공하는 Request 객체가 아니라). 그럴 땐 @nestjs/core의 REQUEST를 이용한다.
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(REQUEST) private request: Request) {}
}
Inquirer provider, 주입되는 자식이 부모 이름을 알게 하기!
If you want to get the class where a provider was constructed, for instance in logging or metrics providers, you can inject the INQUIRER
token.
AppService ← HelloLoggingService 이런 주입 관계가 있다고 할 때, HelloLoggingService 입장에서 부모 provider인 AppService의 정보를 열람할 수 있도록 할 수 있다. @Inject(INQUIRER)를 자식 provider의 생성자 시그니처에 달아주고 부모 provider를 주입(’요구하도록’)하면 된다!
// 자식 provider
import { Inject, Injectable, Scope } from '@nestjs/common';
import { INQUIRER } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class HelloLoggingService {
constructor(@Inject(INQUIRER) private parentClass: object) {}
sayHello(message: string) {
console.log(`${this.parentClass?.constructor?.name}: ${message}`);
// this.parentClass?.constructor?.name = "AppService"
}
}
// 부모 provider
import { Injectable } from '@nestjs/common';
import { HelloService } from './hello.service';
@Injectable()
export class AppService {
constructor(private helloLoggingService: HelloLoggingService ) {}
getRoot(): string {
this.helloLoggingService.sayHello('My name is getRoot');
return 'Hello world!';
}
}
=>
AppService#getRoot() 호출시 "AppService: My name is getRoot" 가 콘솔에 출력된다!
⇒ 쌍방으로 생성자에 주입해준다는 게 특징. 그 중 자식이 부모를 주입받을 땐 @Inject(INQUIRER) 필요.
⇒ 클래스의 이름은 클래스명.constructor.name
으로 알 수 있다.
Performance
request-scoped provider는 매 HTTP 요청시마다 새 인스턴스를 만들어야 하기 때문에 앱이 느려진다. 되도록이면 default singleton scope으로 사용하도록 한다.
Durable providers
provider 중 하나만 request-scoped이어도 부모 controller는 자동으로 request-scoped이 되기 때문에 letency가 증가한다는 문제점을 해결하기 위한 방법 : 10명의 고객(사용자)가 있으면 10개의 독립된 DI sub-trees를 만들자.
(생략)
참고:
[Nest.js] Module Reference, 모듈 참조 클래스
[Nest.js] Module Reference, 모듈 참조 클래스
ModuleRef는 모듈 자체에 대한 정보를 갖고 있는 클래스를 말한다.
기본적으로 모듈 내부의 providers 리스트를 가지고서 어떤 특정한 provider를 찾아달라는 토큰을 받으면 그 참조값을 찾아준다. static과 scoped providers 모두를 동적으로 초기화해주는 기능도 갖고 있다.
ModuleRef.get(DefaultScopedService)과 ModuleRef.resolve(TransientScopedService)가 대표 메소드이다.
static instance of controller, injectable(provider, guard, interceptor, factory, etc.)
: 현재 모듈에서 초기화된(=존재하는) 인스턴스를 말함.
= deault-scoped provider, 즉 앱이 시작될 때 한꺼번에 초기화되어 존재하는 singleton 인스턴스들을 말한다.
TestModule.get(주입 토큰/클래스)로 해당 토큰/클래스의 초기화된 인스턴스를 얻는다.
// cats.service.ts
@Injectable()
export class CatsService implements OnModuleInit {
private service: Service;
constructor(private moduleRef: ModuleRef) {}
onModuleInit() {
this.service = this.moduleRef.get(Service);
}
}
- 다른 모듈에서 inject된 provider 인스턴스를 얻고자 한다면(=global한 문맥에서 provider 인스턴스를 얻고자 한다면) 이렇게 하면 된다:
this.moduleRef.get(Service, { strict: false });
scoped provider (transient or request-scoped)
: 동적으로 ‘해석되어’ 얻는 provider.
= 앱을 실행시킬 때 ‘문맥에 따라 해석되어야만 얻어지는’ provider.
= DEFAULT 타입 제외 REQUEST(HTTP 요청마다 독자적인 인스턴스 가짐)와 TRNASIENT(consumer마다 주입받는 provider의 새 인스턴스를 가짐) 타입 provider들.
TestModule.resolve(주입 토큰/클래스)로 해당 토큰/클래스의 유니크(=singleton이 아닌) 인스턴스를 얻는다. 독자적인 DI container sub-tree가 가지는 context identifier로부터 해석된.
// cats.service.ts
@Injectable()
export class CatsService implements OnModuleInit {
private transientService: TransientService;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.transientService = await this.moduleRef.resolve(TransientService);
}
}
따라서 이 resolve()를 여러 번 호출하여 얻은 인스턴스들은 ‘주소값’이 같은 인스턴스가 아니다.
반대로, context identifier를 동일한 것으로 넣어준다면 resolve()로부터 여러 번 얻는 인스턴스의 주소값이 같도록 만들 수 있다. REQUEST나 TRANSIENT scoped provider를 얻어야 해서 ModuleRef.resolve() 메소드를 써야 하지만 그 때마다 동일한 인스턴스가 반환되는 것이 필요할 때 활용할 수 있다.
- 근데 이렇게 하려면 아래처럼 thsi.moduleRef.resolve를 여러번 부를 게 아니라 그냥 처음에 한 번 불러서 const로 저장해놓으면 되잖아?
어쨌든 구체적인 스텝은:
⇒ ContextIdFactory.create()로 context identifier 생성 ⇒ .resolve(주입 토큰/클래스, 얻은 context identifier)로 호출함으로써 여러 번 호출해도 같은 인스턴스를 얻도록 보장함.
// cats/service.ts
import { ContextIdFactory } from @nestjs/core
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const contextId = ContextIdFactory.create();
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(transientServices[0] === transientServices[1]); // true
}
}
- scoped provider? ⇒ 아! provider의 scope 종류가 DEAULT, REQUEST, TRANSIENT이 있는 중에 request-scoped와 transient-scoped providers를 말하는 것이다.
DI sub-tree와 Context Identifier, providers의 관계
DI container의 sub-tree마다 자신만의 Context Identifier가 활동하고, 그것이 그 ‘문맥(=DI sub-tree)’ 안의 providers를 파악한다.
DI sub-tree —— Context Identifier —— providers
ex) 같은 “HTTP 요청 맥락”이란 같은 DI container sub-tree를 공유한다는 뜻이다.
ex) 같은 Context Identifier를 사용하면 같은 DI sub-tree인 것이다.
Registering REQUEST
provider, 커스텀 context identifier에 커스텀 REQUEST provider 등록하기
위에서처럼 const contextId = ContextIdFactory.create()으로 커스텀 context idenfitier를 만들었다면, 앱의 시작부터 존재하는 static(=default-scoped) provider들은 DI sub-tree의 provider 명단에 잘 들어있지만 REQUEST 타입 provider들은 undefined로 정의되어 있게 된다.
- (TRANSIENT 타입 provider들은..? )
그래서 따로 REQUEST provider들을 새로 만든 context identifier에 등록해주는 과정이 필요하다:
const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(/* YOUR_REQUEST_OBJECT */, contextId);
- 혹시 DI sub-tree(와 거기 그 관리자 context identifier) 자체가 ‘HTTP 요청마다 새 provider 인스턴스가 생성되어야 하는’ 상황에 특화되어 탄생한 개념인가? 그러면 request-scoped provider에 대해서만 따로 처리를 해주는 게 이해가 된다.
- 즉, DI sub-tree의 ‘context(문맥)’이란, consumer(=controller)가 달라지는 것은 아무 영향 없고 HTTP 요청이 발생함에 따라 갈리게 되는 건…가? DI란 단어 뜻 그대로 보면 전자가 더 말이 되는 것 같은데.
- ⇒ 위의 예시 “ex) 같은 “HTTP 요청 맥락”이란 같은 DI container sub-tree를 공유한다는 뜻이다.”, “ex) 같은 Context Identifier를 사용하면 같은 DI sub-tree인 것이다.“로 보건데, context identifier에서 context란 HTTP 요청 ‘context’를 말하는 게 맞는 것 같다…!
Getting current sub-tree,
한 HTTP 요청 ‘안에서’ 다른 request-scoped provider를 (해석하여) 접근하고 싶다?
현재의 ‘요청 맥락’을 관리하는 context idetifier를 얻어내서 목표 provider가 이를 동일하게 가지도록 하면 결과적으로 같은 HTTP 요청 맥락 안에 있게 된다 :
// cats.service.ts
@Injectable()
export class CatsService {
constructor(
@Inject(REQUEST) private request: Record<string, unknown>, // REQUEST는 express나 fastify의 Request 객체가 아닌 날 것의 HTTP 객체를 조회하게 해준다고 했었다.
) {}
const contextId = ContextIdFactory.getByRequest(this.request);
const catsRepository = await this.moduleRef.resolve(CatsRepository, contextId);
}
Instantiating custom classes dynamically
To dynamically instantiate a class that wasn't previously registered as a provider, use the module reference's create()
method.
framework container 바깥에서 원하는대로 다른 클래스들을 provider로써 초기화해줄 수 있는 테크닉이라고 하는데…
- 어떻게 ‘바깥’이라는 건지 모르겠다. 어차피 CatsService를 정의하는 내부에서 CatsFactory도 넣어주고 있는 거잖아..?
// cats.service.ts
@Injectable()
export class CatsService implements OnModuleInit {
private catsFactory: CatsFactory;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.catsFactory = await this.moduleRef.create(CatsFactory);
}
}
참고:
Uploaded by N2T