깊은바다거북
개발 공부 기록
깊은바다거북
전체 방문자
오늘
어제
  • 분류 전체보기 (219)
    • JAVA (9)
    • JavaScript (15)
    • 스파르타코딩클럽 (11)
      • [내일배움단] 웹개발 종합반 개발일지 (5)
      • [내일배움캠프] 프로젝트와 트러블 슈팅 (6)
    • SQL | NoSQL (4)
    • CS 등등 (0)
    • TIL | WIL (173)
    • 기타 에러 해결 (3)
    • 내 살 길 궁리 (4)

인기 글

최근 글

최근 댓글

태그

  • 최대 힙(Max Heap)
  • leetcode-cli
  • TypeScript
  • TIT (Today I Troubleshot)
  • 점화식(Recurrence Relation)
  • Linked List
  • 자바스크립트 기초 문법
  • 트러블 슈팅 Troubleshooting
  • Til
  • Inorder Traversal(중위 순회)
  • 코딩테스트 연습문제
  • Leetcode
  • 재귀 함수
  • 자료 구조
  • Trie
  • 팀 프로젝트
  • Backtracking(백트래킹)
  • 혼자 공부하는 자바스크립트
  • 01. 미니 프로젝트
  • BFS(너비우선탐색)
  • POST / GET 요청
  • Binary Tree(이진 트리)
  • 프로그래머스
  • Preorder Traversal(전위 순회)
  • DFS(깊이우선탐색)
  • 최소 힙(Min Heap)
  • tree
  • BST(이진 탐색 트리)
  • 시간 복잡도
  • 자잘한 에러 해결
hELLO · Designed By 정상우.
깊은바다거북

개발 공부 기록

TIL | WIL

3/8 수 (팀 커밋 방식을 조율하다) TIL, TIT

2023. 3. 9. 02:02

(최종 프로젝트 진행중)

오늘 한 일:

깃 cherry picking 방법을 배워서 원격 main에 잘못 push한 실수를 바로잡았다.

내 파트 중 기본 기능 ‘품앗이 CRUD’를 커밋하였다.

팀의 커밋 방식과 PR - .. - pull 사이클을 (수많은 대화 끝에) 조금 수정했다. 이제는 정말 기본 설정을 거의 마쳐서, 냅둬도 굴러가게 될 것 같다.

깃과 씨름하고 API를 실험 작동하다보니 테스트코드가 너무나 필요해졌다. 작성에 뛰어들었다.

(여기 보던 중: Nest.js module-ref)

Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reactive Programming).
https://docs.nestjs.com/fundamentals/module-ref

(그 전에 보던 중: Nest.js testing#end-to-end-testing)

Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reactive Programming).
https://docs.nestjs.com/fundamentals/testing#end-to-end-testing


※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※

[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 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

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, 주입 생애주기

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를 만들자.

(생략)

참고:

Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reactive Programming).
https://docs.nestjs.com/fundamentals/injection-scopes

[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);
  }
}

참고:

Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reactive Programming).
https://docs.nestjs.com/fundamentals/module-ref


Uploaded by N2T

    'TIL | WIL' 카테고리의 다른 글
    • 3/10 금 (구현할 기술 파트와 범위를 구체적으로 재설정하다) TIL, TIT
    • 3/9 목 (Nest.js로 처음 하는 테스팅) TIL, TIT
    • 3/7 화 (GitHub Actions와 Nest.js 네이밍 컨벤션, 그리고 프로젝트 모듈명 바꾸기 노가다) TIL, TIT
    • 3/6 월 (N2T가 말썽이다) TIL, TIT
    깊은바다거북
    깊은바다거북

    티스토리툴바