Modules

Module은 @Module() 의 주석이 달린 class이다. @Module() 은 Nest에게 application의 구조를 조직하는데 필요한 metadata를 제공한다.


@Module() : NestJS 어플리케이션의 구조를 정의하는 데 사용된다. 모듈은 특정 기능을 캡슐화하여 애플리케이션을 더 구조적으로 만들고, 유지 보수성을 높여준다. 


각 application은 적어도 한 개의 모듈, 즉 root module을 가진다.
root module은 application graph(Nest가 module을 resolve하고 또 provider간의 관계와 의존성을 해결하기 위해 사용하는 internal data structure)를 build 하기위한 Nest의 시작점이다. 이론적으로 매우 작은 application들은 단지 하나의 root module만을 가지지만, 일반적인 경우는 아니다. module은 component를 조직하는 효과적인 방법으로 강력히 권장된다. 즉, 대부분의 application에서 최종 아키텍처는 여러 모듈을 사용하여 구성된다. 각 모듈은 밀접하게 관련된 기능 집합을 캡슐화한다.


@Module() decorator은 한 개의 object를 가지고, object의 속성들은 module을 설명해준다.


properties describe
providers Nest injector에 의해 인스턴스화된 providers이다. 이들은 적어도 이 module 내에서는 공유된다.
controllers 이 module 내에서 정의되고 인스턴스화된 controllers의 집합
imports 이 module에서 사용할 import된, 그리고 다른 module에서 export된 provider들의 리스트
exports providers의 subset이다. 이 module 내에서 정의되었지만 다른 module에서 사용할 필요가 있는 provider들이다. 이를 통해 모듈 간의 의존성을 관리하고, 재사용성을 높일 수 있다.

module은 provider들을 기본적으로 캡슐화한다. NestJs의 모듈 시스템에서는 provider가 module 내에서 선언되고, 다른 module에서 사용하려면 export되어야 한다. 이는 module 간의 의존성을 명확하게 관리하고, 의도하지 않은 의존성 주입을 방지한다.



Feature modules


CatsControllerCatsService는 같은 application domain에 속한다. 둘은 긴밀히 관련되어 있기에, 이를 하나의 기능 모듈(feature module)로 이동하는 것이 합리적이다. feature module은 관련된 기능을 가진 code들을 묶고, 유지하고 명확한 경계를 세워준다. 이러한 방식은 복잡함을 해소하고 SOLID원칙을 고수하게 해준다. 특히 application이 커지고, team이 성장할때 더 중요해진다.


아래는 CatsModule이다.


import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

<팁!>
$ nest g module cats를 통해 간단히 만들 수 있다.


위에서 간단히 **cats.module.ts**안에서 **CatsModule**을 만들 수 있다. 그리고 관련된 파일들을 **cats** 디렉터리로 옮긴다. 마지막으로 해야할 것은 이 module을 root module에서 import 해온다.


import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}
src
    cats
        dto
            create-cat.dto.ts
        interfaces
            cat.interface.ts
        cats.controller.ts
        cats.module.ts
        cats.service.ts
    app.module.ts
    main.ts


Shared modules


NestJS에서 모듈은 기본적으로 SingleTon이다. 즉, 어플리케이션 내에서 모듈이 한 번만 인스턴스화되며, 이로 인해 여러 모듈 간에 동일한 provider instance를 쉽게 공유할 수 있다.



모든 module은 자동적으로 shared module이다. 한번 만들어두면 다른 module에서 재 사용 가능하다. 만일 CatsService의 instance를 다른 module 사이에서 공유하고 싶다고하자. 이를 위해서는 CatsService providerexport에 추가해야 한다.


import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

이를 통해 다른 module에서 CatsModule를 import해서 CatsService에 접근 할 수 있다. 그리고 같은 instance를 다른 모든 module에서 import해서 사용할 수 있다.



Module re-exporting


NestJS에서는 내부 provider를 export할 수 있다. 또한 한 module 내에서 import한 것을 다시 export할 수 있다. 아래의 예시처럼 CommonModule은 CoreModule에서 import과 export를 둘 다 하고 있다. 이를 통해 모듈 간의 의존성을 보다 효과적으로 관리하고, 재사용성을 높일 수 있다.


@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}


Dependency injection


module class는 provider를 inject할 수 있다. (e.g. 주로 설정 또는 초기화 작업을 위해 사용된다. )

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}

하지만 module class 그 자체는 provider로써 injected 될 수 없다. 왜냐하면 Circular dependency가 발생하기 때문이다.
https://docs.nestjs.com/fundamentals/circular-dependency



Global modules

만일 모든 곳에서 같은 module의 집합을 import해야한다면, 이는 꽤 tedius한 작업일 것 이다. Nest와는 달리 Angular providers는 global scope로 등록된다. 한번 정의되면, 어디에서든지 사용할 수 있다. 하지만 Nest는 provider들을 module 내부에서 캡슐화 한다. 그래서 캡슐화된 module을 import해야 사용할 수 있다.


만일 NestJS에서 어플리케이션 전체에서 기본적으로 사용할 수 있는 provider 세트를 제공하려면, module을 global module로 설정하면 된다. 이를 위해 @Global() 데코레이터를 사용한다. 글로벌 모듈로 설정된 모듈은 어플리케이션의 모든 모듈에서 자동으로 사용 가능하다.


import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

@Global() decorator은 module을 global-scope로 만들어준다. Global module은 일반적으로 root module이나 core module에서 한 번만 등록해야 한다. 위의 경우 CatsService provider는 어디에서든지 사용할 수 있다. 만일 이를 사용하고 싶은 module이 있다면 굳이 CatsModule을 import 할 필요가 없다.


<팁!>
global module은 boilerplate를 줄이는데 유용지만, 모든것을 global하게 만드는 것은 좋은 설계가 아니다. 일반적으로 module의 API를 소비자에게 제공하는 가장 권장되는 방법은 imports 배열을 통해 명시적으로 module을 import 하는 것이다.



Dynamic modules

Nest module system은 dynamic module이라는 기능을 제공한다. 이는 동적으로 module을 customize할 수 있다. 동적 module은 provider를 동적으로 등록하고 구성할 수 있는 customizable module을 쉽게 생성할 수 있게 해준다.
https://docs.nestjs.com/fundamentals/dynamic-modules

다음은 DatabaseModule을 위한 dynamic module 정의이다.

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

// forRoot: 이 정적 메서드는 동적 모듈을 반환한다. 기본적으로 entity와 opton을 받아 DB 제공자를 생성한다.
// createDatabaseProviders 함수를 호출하여 DB 제공자들을 생성하고, 이를 proividers와 exports 배열에 추가한다.

@Module({
  providers: [Connection],
  exports: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

<팁!>
forRoot() method은 동기 혹은 비동기적인 dynamic module을 반환한다.(i.e. promise)


이 module은 기본적으로 connection provider를 정의한다. 허나 추가적으로 (forRoot() method에 전달되는 entity나 option들에 의존하여) providers, repository들 따위를 드러내준다. dynamic module이 반환하는 속성들은 @Module() 데코레이터에 정의된 metadata를 덮어쓰기 않고 확장한다. 이것이 정적으로 선언된 connection provider와 dynamic하게 생성된 repository providers들이 이 모듈에서 export되는 방법이다.


만일 dynamic module을 global scope에서 등록하고자 한다면, globla 속성을 true로 설정한다.


{
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
}

DatabaseModule은 다음과 같은 방식으로 imported 될 수 있고 configured 될 수 있다.


import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

만일 DatabaseModule을 export하고자 한다면 다음과 같이 forRoot()를 생략한다.


import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule],
})
export class AppModule {}

Dynamic Module에서 더 자세히 다룰 것이다. working example 과 함께.

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Exception filters  (0) 2024.06.05
Middleware  (1) 2024.06.04
Providers  (0) 2024.06.04
Controllers  (0) 2024.06.03
First Steps  (0) 2024.06.03

Provider

Provider는 Nest의 가장 근본적인 개념이다. 많은 basic한 Nest class들은 provider(service, repository, factory, helper...)로 다루어진다. Provider의 주된 개념은 dependency로서 Provider 주입(injected) 되는 것이다. NestJs에서 객체들은 다양한 관계를 형성할 수 있으며, 이러한 객체들을 "연결(wiring up)"하는 기능은 대부분 Nest 런타임 시스템에 위임될 수 있다.

provider: NestJS에서 Provider는 주입 가능한 서비스, 클래스, 값 또는 기타 의존성으로, 의존성 주입 시스템의 핵심 요소이다. 
제공자는 주로 서비스의 형태로 사용되며, 다양한 구성 요소 간에 재사용 가능한 로직을 캡슐화 한다.

Controller들은 HTTP request들을 다루고, 또 providers에게 복잡한 일들을 위임한다. Providers는 일반적인 JS class이다. 이는 module안에서 provider로서 선언된다.

NestJS는 객체 지향적으로 설계 및 의존성을 조작할 수 있는 기능을 제공하므로, SOLID 원칙을 따르는 것을 권장한다.



Services

Service는 보통 data의 storage, retrieval에 책임이 있다. 그리고 이는 Controller에 의해 사용된다.

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

CLI의 $ nest g service cats command을 통해 쉽게 만들 수 있다.

CatService는 간단한 class, 하나의 property와 두 개의 method이다. 그리고 @Injectable() decorator을 사용한다. 이는 NestJS의 종속성 주입 시스템에서 클래스를 관리할 수 있게 해주는 메타데이터를 클래스에 부착한다.

@Injectable(): 이는 NestJs에서 Provider를 정의할 때 사용된다. 이 decorator는 class가 의존성 주입 시스템에 의해 관리될 수 있도록 지정한다.
    @Injectable() 데코레이터가 있는 클래스는 다른 클래스에 주입될 수 있으며, NestJs가 이를 관리하여 필요한 곳에 자동으로 주입한다.(IoC)

일단은, Cat interface를 사용하기에 아래와 같이 작성한다.

export interface Cat {
  name: string;
  age: number;
  breed: string;
}

이후 작성한 CatsService를 CatController에 가져온다.

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsService는 class constructor에 의해 injected 되었다. 여기서 private 문법을 사용하였다. 이 shorthand는 catsService의 선언과 초기화를 동시에 해준다.

IoC(Inversion of Control)

Don't call us. We'll call you - Hollywood Principle


프레임워크를 적용하지 않은 경우를 생각해보자.
객체의 생명 주기 즉, 객체의 생성, 초기화, 소멸, 메서드 호출 등등을 클라이언트 구현 객체가 직접 관리한다. 또한 다른 사람이 작성한 외부 코드(라이브러리)를 호출하더라도 해당 코드의 호출 시험 역시 직접 사람이 관리한다.


하지만 NestJs와 같은 프레임워크를 사용할 때는 Controller, Service 같은 객체들의 동작을 직접 구현하기는 하지만, 해당 객체들이 어느 시점에서 호출될 지는 신경쓰지 않는다. 프레임워크가 요구하는대로 객체를 생성하면, 프레임워크가 해당 객체들을 가져다가 생성하고, 메서드를 호출하고 소멸시킨다. 즉 프로그램의 제어권이 역전된 것이다.


라이브러리를 사용하는 어플리케이션은 제어 흐름을 라이브러리에 내주지 않는다. 단지 필요한 시점에 라이브러리에 작성된 객체를 적재적소에 가져다 쓸 뿐이다. 하지만 프레임워크를 사용한 어플리케이션의 경우, 어플리케이션 코드에 작성한 객체들을 프레임워크가 필요한 시점에 가져다가 프로그램을 구동하기에 프로그램의 제어권이 프레임워크로 역전된다.


즉 IoC란, 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부의 프레임워크나 라이브러리가 제어 흐름을 결정되는 것을 의미한다. 객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유집 보수를 편하게 할 수 있게 한다.


기존에는 다음과 순서로 객체가 만들어지고 실행되었다.

  1. 객체 생성
  2. 의존성 객체 생성: 클래스 내부에서 생성
  3. 의존성 객체 메소드 호출

하지만 Spring, Nest에서는 다음과 같은 순서로 객체가 만들어지고 실행된다.

  1. 객체 생성
  2. 의존성 객체 주입: 스스로 만드는 것이 아니라 제어권을 프레임워크에게 위임하여 프레임워크가 만들어놓은 객체를 주입한다
  3. 의존성 객체 메소드 호출

Nest가 모든 의존성 객체를 Nest가 실행될 때 다 만들어주고 필요한 곳에 주입시켜준다.


class B{
    //
}


class A{
  // 클래스 A가 클래스 B를 사용하기에 A는 B에 의존적
  // 그렇기에 B가 변하면 A도 변해야하는, 즉 영향을 미치는 관계 A -> Bf
    new b = new B()
}

이번에는 NestJs에서의 IoC를 살펴보자


import {Controller, Get} from '@nestjs/common'
import {AppService} from './app.service'

export class AppController{
    // 1. 사용하고 싶은 서비스 타입 객체를 미리 선언한다.
      private appService: AppService
      //  2. 생성자에서 실제로 사용할 서비스 객체를 직접 생성(binding)한다. 
    constructor(){
        this.appService = new AppService();
    }
  ...
}

위의 코드에서는 개발자가 사용하고 싶은 객체가 있으면 이것을 개발자가 생성부터 소멸까지 직접 관리해야 했다.
이렇게 하면 AppService가 변경이 되었을 때 개발자도 그에 맞춰 코드를 수정해야 한다.
IoC를 활용하면 객체의 생명주기 관리 자체를 외부에 위임한다.(이번의 경우 Nest.js IoC 컨테이너에 위임)


IoC는 모듈 간 결합도를 낮추기에 하나의 모듈이 변경되어도 다른 모듈들에는 영향을 최소화되어 웹 어플리케이션을 지속 가능하고 확장성 있게 해준다.


  • 라이브러리
    • 제어권이 나에게 있다.
    • 내 코드가 필요할 때마다 내가 사용하고 싶은 라이브러리를 사용한다.
  • 프레임워크
    • 제어권이 프레임워크에 있다.
    • 나의 코드를 프레임워크가 필요로 하는 경우에 프레임워크가 알아서 실행시킨다.

DI는 이러한 IoC를 수행하는 하나의 방법이며 Nest.js에서는 생성자를 통한 DI를 가장 기본적인 IoC테크닉으로 생각하고 있다.


    constructor(private readonly appService: AppService){} 

추가적으로 SOLID 원칙의 DIP(의존 역전 원칙) 를 같이 보면 확실히 더 이해가 될 것이다.



DI(Dependency injection)

Nest는 Dependency Injection으로 알려진 design pattern을 사용한다. Argular doumentation 참고


NestJS에서는 Typescript의 기능 덕분에 의존성을 매우 쉽게 관리할 수 있다. 의존성은 타입에 의해 자동으로 해결된다. 아래의 예제에서는 NestJS가 CatService의 인스턴스를 생성하여 catsService로 주입한다. 만약 이미 다른 곳에서 요청된 적이 있다면 기존의 싱글톤 인스턴스를 반환한다. 이러한 의존성은 컨트롤러의 생성자에 전달되거나 지정된 속성에 할당된다.

constructor(private catsService: CatsService) {}


Scopes

Provider은 application의 lifecycle과 동기화 되어있는 수명을 가진다. 이는 application이 bootstrap되었을 때, 모든 dependency가 해결되고, 즉 모든 provider는 인스턴스화 된다. 유사하게 만일 application이 shut down 될 때, 각 provider는 파괴된다. 하지만 provider의 lifetime을 요청 단위(request-scoped) 로 만들 수 있다.
https://docs.nestjs.com/fundamentals/injection-scopes

bootstrap: 어플리케이션을 시작하고 초기화하는 과정을 의미한다. 어플리케이션이 부트스트립되면, 모든 의존성이 해결되고 모든 제공자가 인스턴스화되어야 한다.
provider: NestJs의 어플리케이션에서 의존성을 주입할 수 있는 기본 구성 요소이다. 
    service, repository, factory, helper 등과 같은 클래스들이 provider로 사용된다.
singleton:  클래스의 인스턴스가 어플리케이션 전체에서 하나만 존재하도록 보장하는 디자인 패턴


Custom Providers

Nest는 Provider들의 관계를 정리해주는 IoC container를 가진다. 이 특징은 dependency injection feature의 근간이 된다. 하지만 지금까지 서술했던 것 보다 더 중요한게 있다. provider를 정의하는 몇 가지 방법이 있다. 기본적으로 클래스, 값, 또는 asynchronous 및 synchronous factories를 사용할 수 있다.
https://docs.nestjs.com/fundamentals/dependency-injection



Optional providers

때때로, 반드시 해결되지 않아도 되는 dependency가 있을 수 있다. 예를 들어, 클래스가 configuration object에 의존하지만, configuration object가 제공되지 않으면 기본 값을 사용해야 하는 경우가 있다. 이런 경우에, dependency가 optional 될 수 있다.
왜냐하면 configuration provider의 lack은 error로 취급되지 않기 때문이다.

provider가 optional임을 나타내기 위해, @Optional() decorator를 사용할 수 있다.

import { Injectable, Optional } from '@nestjs/common';

@Injectable()
export class SomeService {
  constructor(@Optional() private readonly configService?: ConfigService) {}

  getConfig() {
    if (this.configService) {
      return this.configService.getConfig();
    }
    return 'default config';
  }
}
//예2
import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(
    @Optional()
    @Inject('HTTP_OPTIONS') 
    private httpClient: T) {}
}

위의 코드(예2)에서는 HTTP_OPTIONS custom Token을 포함하기에 custom provider를 사용한다. 이전 예제들은 클래스의 생성자에서 constructor-base injection을 통해 dependency를 나타냈다.
https://docs.nestjs.com/fundamentals/custom-providers

custom provider: 기본 제공 provider 외에 개발자가 직접 정의한 provider
token: provider를 식별하는 데 사용되는 고유한 값이다. 문자열, simbol, class를 토큰으로 사용할 수 있다.


Property-based injection

지금까지는 constructor-based의 기술이였다. class의 constructor method를 통해 provider들이 주입되는 거였다. 이번에 서술할 property-based injection도 유용할 것이다. 예를 들어, top-level의 class가 하나의 혹은 여러개의 provider에 의존한다면,
sub-class들의 constructor의 super() 를 사용할 것인데 이는 매우 tedious하다. 대신에 @Inject() decorator를 사용할 수 있다.

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

주의사항
만일 class가 다른 class를 extend 하지 않는다면, constructor-based injection을 사용해야 한다. constructor는 명백하게 무슨 dependency들이 필요한지 나타내고, @Inject() 로 나타내는 것보다 더 나은 가시성을 제공한다.



Provider registration

provider CatsService, 그리고 이러한 service를 사용하는 CatsController를 가지고 있다. 그리고 이들을 injection을 위해 Nest에 등록해야한다. 이들 module file인 app.module.ts 통해 추가해야한다. 이들을 providers의 array를 @Module() decorator에 추가한다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

Nest는 이제 CatsController class의 의존성을 해결할 것이다.

현재 파일 구조는 다음과 같다.

src
    cats
            dto
                create-cat.dto.ts
            interface
                cat.interface.ts
            cats.controller.ts
            cats.service.ts
    app.module.ts
    main.ts

Manual Instantiation

지금까지, 어떻게 Nest에서 자동적으로 dependency들을 처리하는지를 다뤄왔다. 특정 상황에서는 built-in Dependency Injection system(어플리케이션의 클래스 간 의존성을 자동으로 관리하고 주입하는 메커니즘, IoC 컨테이너)에서 벗어나 수동으로 provider를 검색하거나 인스턴스화해야 할 수도 있다. 아래는 두 가지 주제를 간단히 설명한다.

기존의 instance, instance화가 된 provider를 얻기 위해서는 Module Reference를 사용할수 있다.(ModuleRef)
https://docs.nestjs.com/fundamentals/module-ref

import { ModuleRef } from '@nestjs/core';

@Injectable()
export class MyService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async getDynamicProvider() {
    const provider = await this.moduleRef.create(SomeProvider);
    return provider;
  }
}

bootstrap() 함수에서 providers를 얻기 위해서는 Standalone application을 살펴 봐야한다.
https://docs.nestjs.com/standalone-applications

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Exception filters  (0) 2024.06.05
Middleware  (1) 2024.06.04
Modules  (1) 2024.06.04
Controllers  (0) 2024.06.03
First Steps  (0) 2024.06.03

Controllers

Controllers는 들어오는(incoming) requests와 clinent로 나가는(returning) responses에 대한 책임을 가지고 있다.

Controller는 application에 대한 특정한 requests를 받는다. **routing** 메커니즘은 어떤 controller가 어떤 request를 받는지를 제어한다. 각 controller는 1개 이상의 route를 가진다. 그리고 route 마다 다양한 action들을 취한다.

routing: 클라이언트의 요청 URL을 기반으로 요청을 적절한 컨트롤러와 핸들러 함수에 매핑하는 과정이다.

basic한 controller를 만들기 위해, Class들과, Decorator들을 사용한다. Decorator들은 클래스에 필요한 metadata를 연관시키고, Nest가 routing map을 생성하여 요청을 해당 컨트롤러와 연결할 수 있게 해준다.

metaData: 
    데이터에 대한 정보를 설명하는 데이터. 여기서는 데코레이터를 통해 클래스나 메서드에 추가되는 정보를 의미한다.
routing map:
    클라이언트의 요청 URL을 특정 컨트롤러 메서드에 매핑하는 구조이다.

<팁!>
유효성 검사가 내장된 CRUD 컨트롤러를 빠르게 생성하려면, CLI의 CRUD 생성기를 사용할 수 있다. nest g resource [name]

Routing

아래의 코드는 @Controller() decorator를 사용한다. 이는 가장 기본적인 controller이다. 여기서 route path로 cats인 prefix를 설정한다. @Cotroller() decorator에 path prefix를 기입함으로써 간단히 관련된 routes를 group화 할 수 있다. 그리고 반복되는 코드를 최소화 할 수 있다. 예를 들어, 우리는 cat entity와 관련된 /cats route group들을 선택할 수 있다. 이 경우, @Controller() 데코레이터에 'cats' 경로 접두사를 지정하면 파일 내의 각 경로에 대해 경로 부분을 반복해서 작성할 필요가 없다.

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

<팁!>
CLI를 사용하여 controller를 만들고자 한다면, 간단히 $ nest g controller [name] 커멘드를 사용하면 된다.

findAll()에 붙어있는 @Get() HTTP request method decorator는 Nest에게 HTTP requests의 endpoint에 대한 handler를 만들어라고 명령한다. endpoint는 HTTP request method(위의 경우는 GET)와 route path에 대응된다.

이 때 routh path는 컨트롤러에 선언된 (선택적) prefix와 method의 decorator에 지정된 경로를 연결하여 결정된다. 예를 들어 @Controller에 'cats' 그리고 @Get(':id')라면 전체 path는 '/cats/:id'가 된다. 만일 prefix에 'cats'를 선언한다면, 더 이상 decoraotor에 어떤 path 정보를 기입할 필요가없다. Nest가 자동으로 map 'GET /cat' request를 handler에 mapping 할 것이다. 앞서 언급한 대로, path는 controller의 정보(prefix)와 requests method decorator의 정보를 포함한다. 그렇기에 'cats'와 prefix와 @Get('breed')가 존재한다면 route의 path는 'GET /cats/breed'가 된다.

위의 코드에서 만일 이 endpoint에서 만들어진 GET request가 호출된다면, Nest는 개발자가 만든 findAll() method로 route할 것이다. 이때 findAll()과 같은 method 이름은 개발자가 임의로 지정할 수 있다. path를 binding하기 위해 method를 선언해야 하지만, Nest는 method의 이름에 아무런 의미를 부여하지 않는다.

이 method는 정상 작동을 한다면 200의 status code와 관련된 response를 반환할 것이다.(위의 경우 단순한 string) 이를 이해하려면 Nest가 응답을 조작하는 두 가지 다른 옵션을 알아야 한다.

OPTION DESCRIPTION
Standard (recommended) 만일 내장 함수(built-in method)를 사용하여, request handler가 Javascript Object나 Array를 반환할 때, 이는 자동적으로 JSON으로 직렬화(serialized)된다. 하지만 원시타입(Primitive, e.g. string, number, boolean)을 반환할 때는 별도의 직렬화를 하지 않는다. 이를 통해 응답 처리는 단순해진다. 값만 반환하면 나머지는 Nest가 처리한다. 게다가, 기본적으로 response의 POST는 status code 201을, 나머지는 200을 반환한다. 만일 이를 바꾸고 싶다면 단순히 @HttpCode(...) decorator를 추가하면 된다.
Library-specific 개발자는 library-specific(e.g. Express) response object를 사용할 수 있다. 이는 method handler signature안에 @Res() decorator를 사용하여 주입할 수 있다.(e.g. findAll(@Res() response)) 이러한 접근방식으로, 해당 객체가 드러내는 native response handling methods를 사용할 수 있다. 예를 들어 Express에서, 'response.status(200).send()'와 같은 코드를 사용하여 응답을 구성할 수 있다.
Method handler signature: 메서드의 매개변수와 반환 '타입'을 포함한 정의
native response handling method: Express나 Fastify와 같은 HTTP 서버 라이브러리가 제공하는 응답 처리 메서드로
    상태 코드 설정, 헤더 설정, 응답 본문 전송 등을 수행한다.
    e.g. 
    1. response.status(code): HTTP 응답 상태 코드를 설정
    2. response.send(body): 응답 본문을 전송한다.
    3. response.json(json): JSON 형식의 응답 본문을 전송한다.
    4. response.set(header, value): 응답 헤더를 설정한다.
// 상태코드의 변경
import { Controller, Get, HttpCode } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  @HttpCode(204)
  findAll() {
    return 'No Content';
  }
}
// Express를 기반의 @Res() 활용
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Res() response: Response) {
    const cats = ['cat1', 'cat2', 'cat3'];
    response.status(200).json(cats); // 상태코드 조작 가능
  }
}

주의할점
Nest는 @Res()@Next()의 사용을 감지한다. 이는 개발자가 library-specific option을 사용함을 나타낸다. 허나 만일 Standard approach와 동시에 사용한다면 Standard approach 접근 방식이 자동으로 비활성화되어 정상적으로 동작하지 않는다.
만일 두 가지 접근 방식을 동시에 사용하려면(e.g. cookie/header를 설정하기 위해 response 객체에 주입을 하되 나머지는 프레임워크에 맡기기 위해, 즉 응답 객체를 직접 조작하면서 나머지 응답 처리는 프레임워크에 맡기려면), @Res({passthough:true}) 데코레이터에서 'passthrough' 옵션을 'true'로 설정해줘야 한다.

Request object

Handler는 client의 request에 접근할 필요가 있다. Nest는 request object에 접근을 가능케한다. @Req() decorator를 주입함으로써 request object에 조작할 수 있다.

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

express의 타입 지정을 위해 (위의 예에서는 request:Request), @types/express package를 설치하면 된다.

request object는 HTTP request를 나타낸다. 그리고 request query string, parameters, HTTP headers, body 등을 가진다. 대부분의 경우에 이러한 속성을 수동으로 가져올 필요는 없다. 대신 @Body(), @Query()와 같은 전용 decorator를 통해 접근할 수 있다.

decorators obejct list
@Request(), @Req() req
@Response(), @Res() * res
@Next() next
@Session() req.session
@Param(key?: string) req.params/ req.params[key]
@Body(key?:string) req.body/req.body[key]
@Query(key?:string) req.query/req.query[key]
@Headers(name?:string) req.headers/req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

NestJS는 하위 HTTP 플랫폼(e.g. Express와 Fastify) 간의 타입 호환성을 위해, Nest는 @Res(), @Response() decorator를 제공한다. @Res()는 @Response()의 별칭이다. 둘 다 하위 native platform의 응답 객체 interface를 직접 노출한다. 만일 이 둘을 사용하면, 개발자는 하위 라이브러리(e.g. @types/express)에서 타입을 import 해야한다. 기억해야 할 것이 @Res()@Response()을 method handler에 주입한다는 것은, Nest가 response를 Library-specify mode로 처리함을 뜻한다. 곧, 개발자가 response의 관리에 책임이 있다는 것이다. 그렇기에 개발자는 반드시 response object를 사용하여 어떤 형태로든 응답을 반환해야 한다.(e.g. res.json(...), res.send(...)). 그렇지 않으면 HTTP 서버가 응답을 기다리며 멈추게 된다.

native platform: NestJS와 같은 프레임워크가 작동하는 기본 HTTP 서버 라이브러리를 의미한다. NestJS는 Express와 Fastify와 같은 HTTP 서버 라이브러리 위에서 동작할 수 있다. 이러한 라이브러리를 "native platform"이라고 부르며. 각각의 platform은 고유의 요청 및 응답 객체와 메서드를 제공한다.

method: 클래스 내에 정의된 함수, 객체 지향 프로그래밍에서 객체의 동작을 정의
method handler: 특정 HTTP 요청을 처리하는 함수, 주로 컨트롤러 클래스 내에서 정의된다.

Resources

앞에서 GET route, resoucres를 가져오는 endpoint를 정의내렸다. 이번에는 POST route, 새 record를 만드는 endpoint를 만들 것이다.

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create() {
    return 'This action adds a new cat';
  }

  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

Nest는 standard한 HTTP method을 위한 decorator들을 제공한다.
Get, Post, Put, Delete, Patch, Options, Head 그리고 All, 이것은 모든 것들 다루는 endpoint이다.

Route wildcards

Pattern을 기반으로한 route도 지원한다. 예를 들어 *(asterisk)는 와일드 카드로 사용되며, 임의의 문자 조합과 일치한다.

import { Controller, Get } from '@nestjs/common';

@Controller('files')
export class FilesController {
  @Get('download/*')
  downloadFile() {
    return 'This action handles downloading files';
  }

  @Get('images/*')
  serveImage() {
    return 'This action serves images';
  }
}

이 밖에 ?, +, * 그리고 () 또한 route path에 쓰인다. 이는 정규 표현식(regular expression)과 유사하다. 하이픈(-), 점(.)은 문자열 기반 경로에서 문자 그대로 해석된다.

주의사항
route의 중간에서의 wildcard는 express에 의해서만 지원된다.

Status code

앞서 언급한 대로 201번인 POST를 제외한 나머지의 상태코드는 200이다. 이는 @HttpCode(...) 를 통해 handler level에서 조작할 수 있다. @nestjs/common package에서 import한다.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

만일 상태 코드가 static하지 않고 다양한 요소에 의존한다면, library-specific response object(inject using @Res())를 사용하면 된다.

Headers

custom response header을 사용하기 위해서는 @Header() decorator이나, library-specific response object를 사용한다.( res.header() 을 통해 직접적으로) Header는 @nestjs/common package를 통해 import한다.

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Redirection

redirection은 클라이언트가 요청한 URL을 다른 URL로 자동으로 전환시키는 웹 기술이다. 웹 서버 또는 어플리케이션은 클라이언트에게 HTTP 응답으로 특정 상태 코드와 새로운 URL을 보내어 클라이언트가 해당 URL로 이동하도록 유도한다.

response를 다른 URL로 redirection하기 위해서는, @Redirect() decorator를 사용하거나 library-specific response object의 res.redirect() 를 사용한다.
@Redirect() 는 두 가지 argument를 필요로 한다. urlstatusCode이다.(둘 다 optional) status의 default value는 302이다.

@Get()
@Redirect('https://nestjs.com', 301)

만일 개발자가 HTTP 상태 코드와 redirect URL을 동적으로 하고 싶다면, HttpRedirectResponse interface(@nest/common)을 사용한다.

method handler의 반환값이 존재한다면 @Redirect()의 값을 덮는다.

import { Controller, Get, Redirect } from '@nestjs/common';

@Controller('example')
export class ExampleController {

  // 기본적으로 'https://default-url.com'로 302 상태 코드로 리다이렉트
  @Get('default-redirect')
  @Redirect('https://default-url.com', 302)
  defaultRedirect() {
    // 이 메서드가 반환하는 값이 없으므로, 데코레이터의 URL과 상태 코드가 사용됨
  }

  // 리다이렉션 URL과 상태 코드를 동적으로 변경
  @Get('dynamic-redirect')
  @Redirect('https://default-url.com', 302)
  dynamicRedirect() {
    return { url: 'https://dynamic-url.com', statusCode: 301 };
    // 반환 값이 있으면, 이 값이 데코레이터 인수를 덮어씀
  }
}

Route parameters

static path로 인한 Route는 만일 개발자가 request의 부분으로서 dynamic data를 받기에 부적절하다.
예를 들어 GET /cats/1에서의 id값 1이 있다. NestJs에서는 경로에 동적 매개변수 토큰을 추가하여 요청 URL의 해당 위치에서 동적 값을 얻을 수 있다. 이러한 경로 매개변수는 콜론(':')을 사용하여 정의 된다.
아래의 @Get()에 있는 route parameter token의 사용 예시이다. route parameter는 @Param() 을 통하여 접근할 수 있다. param()은 method signature을 통해 추가 된다.

method signature: 메서드의 이름, 매개변수 리스트, 반환 타입을 포함한 메서드의 정의를 나타낸다.
//TS
@Get(':id')
findOne(@Param() params: any): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}


//TS-타입 지정
@Get(':id')
findOne(@Param() params: {id:string}): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
@Get(':id')
@Bind(Param())
findOne(params) {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
//Bind() 데코레이터는 여러 매개변수 데코레이터를 결합하여 하나의 매개변수 객체로 전달할 수 있게 해준다.

NestJS에서 경로를 선언할 때, 매개변수가 있는 경로는 반드시 정적 경로 뒤에 선언해야 한다. 이렇게 해야 매개변수가 있는 경로가 정적 경로를 향하는 트래픽을 가로채지 않기 때문이다. path mapping은 선언된 순서대로 이루어진다. 만약 parameter가 있는 path를 static path보다 먼저 선언하면, 모든 해당 path가 매개변수 경로로 매칭되어 static 경로가 동작하지 않게 된다.

import { Controller, Get, Param } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get(':id')
  getItem(@Param('id') id: string): string {
    return `Item ID: ${id}`;
  }

  @Get('static')
  getStatic(): string {
    return 'This is a static route';
  }
}

잘못된 경우 : 매개변수가 있는 경로를 정적 경로보다 먼저 선언한 경우
'/items/static' 경로로 요청이 들어오면, 이 요청은 매개변수 경로 '@Get(':id')'로 매칭되어 id 파라미터로 인식된다.
따라서 getStatic() method는 호출되지 않는다.

     - 요청 URL: /items/static'
        - 결과: Item ID: static
import { Controller, Get, Param } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get('static')
  getStatic(): string {
    return 'This is a static route';
  }

  @Get(':id')
  getItem(@Param('id') id: string): string {
    return `Item ID: ${id}`;
  }
}

올바른 경우 : 정적 경로를 매개변수가 있는 경로보다 먼저 선언한 경우
'/items/static' 경로로 요청이 들어오면, 정적 경로 '@Get('static')'가 먼저 매칭되므로, 'getStatic()' method 가 호출된다.
매개변수 경로 '@Get(':id')'는 다른 모든 경로를 처리한다.

    - 요청 URL: '/items/static'
        - 결과: 'This is a static route'
    - 요청 URL: '/items/123'
        - 결과: 'Item ID: 123'

@Param() 은 method parameter을 decorate되어, path parameter를 method 내부에서 해당 매개 변수의 속성으로 사용할 수 있게 한다. 이를 통해 요청 경로의 특정 매개변수를 쉽게 접근하고 사용할 수 있다. Param@nestjs/common package에서 import 한다.

//TS
@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}
//JS
@Get(':id')
@Bind(Param('id'))
findOne(id) {
  return `This action returns a #${id} cat`;
}

Sub-Domain Routing

@Controllerhost 옵션을 가질 수 있다. 이를 통해 들어오는 요청의 HTTP 호스트가 특정 값과 일치해야 함을 요구할 수 있다. 이를 통해 특정 호스트에 대해 경로를 바인딩할 수 있다. 즉 특정 도메인에서만 api를 요청할 수 있도록 하는 것이다.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}
  • 위의 경우 'admin.example.com' 호스트에 대해서만 'AdminController'의 path를 활성화 한다.
  • '/' path로 들어오는 요청은 호스트가 'admin.example.com'인 경우에만 처리된다.
HTTP host: 웹 요청을 처리할 서버의 도메인 이름 또는 IP주소를 의미한다.
    웹 브라우저나 클라이언트는 서버에 요청을 보낼 때 요청 헤더의 호스트 정보를 포함시킨다.
    이 호스트 정보는 서버가 어떤 도메인에 대한 요청인지를 식별하는 데 사용된다.

주의사항
Fastify는 nested routers에 대한 지원의 부족으로 만일 sub-domain routing을 해야한다면, Express adapter을 대신 사용해야 한다.

route path와 유사하게, hosts option을 사용하면 host name에서의 dynamic value, host paramter token을 캡쳐할 수 있다. @HostParam() decorator을 통해 host parameter에 접근할 수 있다. method는 method signature로 사용되어야 한다.

import { Controller, Get, HostParam, Param } from '@nestjs/common';

@Controller({ host: ':subdomain.example.com' })
export class SubdomainController {
  @Get('user/:id')
  getUser(@HostParam('subdomain') subdomain: string, @Param('id') id: string) {
    return `This action returns user with id ${id} from the ${subdomain}.example.com subdomain`;
  }
}

Scopes

NestJS에서는 대부분의 리소스가 들어오는 모든 요청에 대해 공유된다. 이러한 점은 다른 프로그래밍 언어 배경을 가진 사람에게는 놀라울 것이다. DB에 대한 연결 풀, 전역 상태를 가진 singleton service 등이 이러한 예이다. 이는 Node.js가 request/response multi Threaded Stateless Model을 따르지 않기 때문이다. 따라서 singleton instance를 사용하는 것이 application에서 완전히 안전하다.

NestJS는 Node.js 기반으로 동작한다. 특히, NestJS는 다수의 요청을 처리할 때 대부분의 componenet를 공유한다.

componenet: 주로 모듈, 컨트롤러, 서비스, 미들웨어, 파이프, 필터, 가드 등을 의미한다. 
    이러한 컴포넌트는 NestJS 어플리케이션의 주요 빌딩 블록으로, 각 컴포넌트는 특정한 역할과 책임을 가지고 있다.
    1. module: 관련된 컴포넌트들을 그룹화하여 구성 요소를 조직화하는 단위. 
        모든 NestJS 어플리케이션은 최소 하나의 루트 모듈을 가지며, 어플리케이션의 구조를 정의
    2. controller: 컨트롤러는 들어오는 요청을 처리하고, 클라이언트에게 응답을 반환하는 역할을 한다. 
        주로 라우팅을 처리하며, 서비스 계층과 상호작용한다.
    3. service: 서비스는 비즈니스 로직과 데이터 접근을 담당한다. 일반적으로 싱글턴으로 동작하며, 
        컨트롤러나 다른 서비스에서 주입받아 사용
    4. middle: request가 controller에 도달하기 전에 처리되는 함수. loggin, authentication, request 변환 등 다양한 목적에 사용
    5. pipe: transformation과 validation을 담당한다. 요청 데이터가 컨트롤러 핸들러로 전달되기 전에 처리된다.
    6. filter: 필터는 예외를 처리하고, 사용자 정의 예외 응답을 생성할 수 있다.
    7. guard: 요청이 컨트롤러 라우트 핸들러에 도달하기 전에 실행되며, 주로 인증 및 권한 부여 로직을 포함한다.
  • NestJS에서의 공유 컴포넌트

    1. database 연결 풀: 여러 요청이 동일한 데이터베이스 연결 풀을 사용한다. 이는 효율성을 높이고 자원 낭비를 줄인다.
      • DataBase Connection Pool: 다수의 DB 연결을 미리 생성해 두고, 요청이 있을 때 재사용할 수 있도록 관리하는 메커니즘. 이는 각 요청마다 새로운 DB 연결을 생성하는 대신, 미리 생성된 연결을 재사용하여 성능을 최적화하고 자원을 절약한다.
      • import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppService } from './app.service'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'test', password: 'test', database: 'test', entities: [__dirname + '/../**/*.entity{.ts,.js}'], synchronize: true, // 연결 풀 설정 extra: { connectionLimit: 10, // 연결 풀 크기 설정 }, }), ], providers: [AppService], }) export class AppModule {}
    2. 싱글턴 서비스: singleton 서비스는 어플리케이션 전역에서 단 하나의 instance로 존재하는 서비스이다. 대부분의 서비스는 싱글턴으로 동작하여 글로벌 상태를 유지할 수 있다. 즉 여러 요청 간에 동일한 인스턴스를 공유할 수 있음을 의미한다. 메모리 사용을 최적화하고, 반복적인 초기화 비용을 줄인다.
    3. 글로벌 상태: application 전역에서 공유되는 데이터나 설정을 의미한다. NestJS에서는 싱글턴 서비스를 통해 이러한 글로벌 상태를 관리할 수 있다. 즉 application 전역에서 상태를 공유하여 전역 설정이나 데이터를 중앙에서 관리할 수 있다.
    4. // 아래의 ConfigService는 어플리케이션 전역에서 설정 값을 관리한다. // 다른 서비스나 모듈에서 ConfigService를 주입받아 설정 값을 읽거나 쓸 수 있다. import { Injectable } from '@nestjs/common'; @Injectable() export class ConfigService { private config = { appName: 'NestJS Application', version: '1.0.0', }; getConfig(key: string) { return this.config[key]; } setConfig(key: string, value: any) { this.config[key] = value; } } @Module({ providers: [ConfigService], exports: [ConfigService], }) export class ConfigModule {}
  • Node.js와 멀티스레딩:

    • Node.js는 요청/응답 모델에서 멀티스레드 상태 비저장 모델을 따르지 않는다. 대신, 단일 스레드 이벤트 루프를 사용하여 비동기적으로 요청을 처리한다. 이러한 특성으로 싱글턴 인스턴스를 사용하는 것이 안전하다.
  • NestJS의 범위(Scopes):

    • NestJS에서는 서비스의 인스턴스 생성 방식을 제어하기 위해 다양한 범위를 제공한다. 각 범위는 서비스 인스턴스의 수명 주기와 범위를 정의한다.
    1. 기본 범위(Default Scope): 기본적으로 NestJS의 모든 서비스는 싱글턴으로 동작한다. 즉, 어플리케이션이 시작될 때 한 번 인스턴스화 되고, 모든 요청에서 동일한 인스턴스를 사용한다.
    2. @Injectable() export class MySingletonService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }
    3. 요청 범위(Request Scope): 요청 범위의 서비스는 각 요청마다 새로운 인스턴스를 생성한다. 이렇게 하면 요청 간에 상태를 공유하지 않는다.
    4. import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) export class MyRequestScopedService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }
    5. 트랜지언트 범위(Transient Scope): 트랜지언트 범위의 서비스는 주입될 때마다 새로운 인스턴스를 생성한다. 이를 주입받는 각 컴포넌트가 고유의 인스턴스를 가지게 함을 의미한다.
    6. import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.TRANSIENT }) export class MyTransientService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }

그러나 일부 경우에는 요청 기반의 라이프사이클이 바람직할 수 있다. 예를 들어, GraphQL application에서의 per-request caching. request tracking, multi-tenancy가 있다.

Multi Threaded Stateless Model: 각 요청이 별도의 스레드에 의해 처리되는 것
SingleTon: 특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴

request-cahcing: 요청별로 캐시를 관리하여 DB 요청을 줄일 수 있다.
request tracking: 각 요청마다 고유한 추적 ID를 생성하여 요청을 추적할 수 있다.
multi-tenancy: 하나의 소프트웨어 인스턴스가 여러 사용자를 동시에 서비스하는 아키텍쳐를 말함
    각 요청마다 다른 tenancy의 데이터를 처리할 수 있다. 
tenancy: 소프트웨어 application을 사용하는 개별 고객 또는 조직을 말한다.

Asynchronicity(비동기성)

Asynchronicity는 작업이 완료될 때까지 기다리지 않고 다음 작업을 수행하는 것을 말한다.
대부분의 data extraction의 경우 asynchronous하게 된다. 그렇기에 Nest에서는 async function을 제공한다.자세히

모든 async function은 Promise를 반환한다. 즉 비동기 작업을 수행할 때 연기된(deferred) 값을 반환할 수 있으며, nest는 이를 스스로 해결할 수 있다. 이는 Promise, async/await를 사용하여 비동적으로 처리되는 값을 반환할 수 있음을 의미한다.

@Get()
async findAll(): Promise<any[]> {
  return [];
}

위의 코드는 유효하다. 게다가 Nest route handler는 RxJS observable streams를 사용함으로써 더 강력하다. 이를 통해 비동기 데이터 흐름을 쉽게 관리할 수 있다. Nest는 내부적으로 observable에 자동으로 구독하고, 스트림이 완료되면 마지막으로 방출된 값을 클라이언트에게 반환한다.

RxJS: Reactive Extensions for JavaScript의 약자로, 비동기 및 이벤트 기반 프로그램을 쉽게 작성할 수 있도록 도와주는 라이브러리다. 

Observable: data stream을 나타내며, 시간이 지남에 따라 여러 값을 방출할 수 있다. 
    - 데이터가 도착했을 때 수행할 작업을 정의하는 구독자(subscriber)를 가질 수 있다.
Observer: Observable에서 방출된 데이터를 처리하는 콜백 함수의 모음. 보통 next, error, complete method를 가진다.
Subscription: Observable에 대해 구독을 시작하거나 중지하는 것을 관리한다.
Operators: Observable의 데이터 스트림을 변형하거나 필터링할 수 있는 함수들이다.

Observable은 RxJS(Reactive Extensions for JavaScript) 라이브러리에서 제공하는 핵심 개념 중 하나로, 비동기 데이터 스트림을 나타낸다. 'Observable'은 데이터의 흐름을 관리하고, 이를 구독(subscribe)하여 데이터가 발생할 때마다 반응할 수 있게 한다. 이를 통해 이벤트 기반의 비동기 프로그래밍을 쉽게 구현할 수 있다.

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

Request Payloads

이전의 POST Example은 any client params를 받지 못한다. 이를 @Body() decorator을 추가함으로써 고칠 수 있다.

먼저 DTO(Data Transfer Object) schema를 추가한다. DTO는 network를 거쳐 어떻게 보내는지를 정의한다. Typescript의 interface를 통해, 혹은 class로 DTO schema를 결정할 수 있다. 신기하게도 class 사용을 권장한다. 왜냐하면, Class들은 JavaScript ES6 standard의 부분이기에 compiled JavaScript의 안에서 real entities로 보존된다. 반면에 Typescript의 interface들은 transpilation 과정에서 삭제된다. 그리고 Nest는 runtime 과정에서 이를 언급하지 않는다. 이는 NestJs에서 중요한 의미를 가지는데, 특히 파이프(Pipe) 와 같은 기능이 런타임에 변수의 메타타입(metatype)에 접근할 수 있을 때 추가적인 가능성을 제공하기 때문이다.

compile: 한 언어로 작성된 소스 코드를 다른 언어로 변환하는 것을 의미한다.
    e.g. Java -> bytecode
         c -> assembly
transpile: 한 언어로 작성된 소스 코드를 비슷한 수준의 추상화를 가진 다른 언어로 변환하는 것
    e.g. es6 -> es5
         c++ -> c
         coffescript -> javascript
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

이는 3개의 간단한 속성을 가진다. 이후에 우리는 새로히 만들어진 DTO를 사용할 수 있다.

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}
// TS
@Post()
@Bind(Body())
async create(createCatDto) {
  return 'This action adds a new cat';
}

ValidationPipe는 method handler로 전달되면 안되는 속성들을 filter해준다. 개발자는 acceptable한 properties들을 whitelist할 수 있다. 그리고 whilelist되지 못한 속성들을 자동으로 stripped 된다.자세히

Full resource error

아래의 경우 몇 개의 decorator를 이용해 만든 basic controller의 예이다. internal data에 접근하고 조작하기 위한 몇개의 method들이 담겨져 있다.

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get() // HTTP 요청의 쿼리 매개변수를 추출하는 데 사용
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

Nest CLI는 위의 작업을 손쉽게 만들어주는, all the boilerplate code를 만들어주는 generator를 제공한다.자세히

Getting up and Running

위의 controller가 정의되었음에도 Nest는 CatsController의 존재를 알지못한다. 그렇기에 이 class의 instance를 만들지 못한다.

Controller는 항상 module에 속한다. @Module() decorator안의 controllers array에 class가 속해야 한다. 아직 예시에서는 AppModule을 제외한 나머지 module을 만들지 않았기에, 우리는 여기서 CatsController를 넣는다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

@Module() Decorator를 사용하여 MetaData를 module class에 첨부하였으며, 이제 Nest는 어떤 controller들이 mount되어야 하는지 쉽게 알 수 있다.

Library-Specific Approach

지금까지 Nest에서 responses들을 조작하는 기본적인 way들을 알아봤다. 두 번째 방법은 library-specific response object이다. response object를 inject하기 위해서, @Res() decorator를 사용해야한다.

//JS
import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  @Bind(Res(), Body())
  create(res, createCatDto) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  @Bind(Res())
  findAll(res) {
     res.status(HttpStatus.OK).json([]);
  }
}
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

이러한 방식은 response object를 직접적으로 제어함(Header manipulation, library-specific features, ...)으로써 좀더 flexibility 하지만 이는 좀더 조심해서 사용해야한다. 일반적으로 상대적으로 덜 clear하고, 일부 단점이 존재한다. 무엇보다도 코드들이 platform-dependent 해진다. 예를 들어 Express에서 Fastify로 변경할 경우, 응답 객체의 API가 달라 코드 변경이 필요하다. 테스트가 복잡해진다. 응답 객체를 직접 mocking 해야하므로 테스트가 더 어려워 진다.

또한 compatibility를 잃을 것이다. 플랫폼 종속적인 접근 방식을 사용하면, interceptors와 @HttpCode(), @Header() decorator 등 Nest의 표준 응답 처리에 의존하는 기능들과의 호환성을 잃게 된다. 이럴 해결하기 위해, passthrough 옵션을 true로 설정할 수 있다.

// TS
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

이제 native response object와 interact를 할 수 있다. (e.g. set cookies, or headers).

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Exception filters  (0) 2024.06.05
Middleware  (1) 2024.06.04
Modules  (1) 2024.06.04
Providers  (0) 2024.06.04
First Steps  (0) 2024.06.03

첫 단추로 Nest의 가장 핵심 기능을 알아 볼 것이다. 가장 기본적인 CRUD에 대해 알아 보자.

Language

주로 TypeScript를 사용할 것이고, 또한 Node.js를 애착한다. 그렇기에 Nest는 Typescript과 pure Javascript 둘 다 양립가능하다. Nest는 가장 최근의 language feature들의 이점을 가져온다. 그래서 vanilla JavaScript와 함께 사용하기 위해 Nest는 Babel compier를 사용한다.

Vanilla JS: 외부의 라이브러리나 프레임워크를 이용하지 않는 순수 자바스크립트를 말한다.
    둘을 사용하지 않기에, 사용했을 때 보다 호환성이 좋다는 게 특징이다. 이런 특징은 디버그(Debug)를 할 때 유용하다. 

Babel Compiler: Javascript 코드를 컴파일하는 도구로, 최신 JS 문법과 기능을 이전 버전의 JS로 변환해준다. 
    이를 통해 최신 Javascript 기능을 지원하지 않는 구형 브라우저나 환경에서도 최신 코드를 사용할 수 있게 해준다.

Prerequisites

사용자의 OS에는 Node.js(version >= 16)이 설치되어야 한다.

Setup

새 프로젝트를 setting하는데는 Nest CLI를 이용하여 꽤 간단하다.

$ npm i -g @nestjs/cli
$ nest new project-name

만일 TypeScript의 stricter한 feature set으로 설치하고자 한다면, --strict flag를 nest new command에 덧붙인다.

project-name 디렉토리가 만들어 질 것이며, node modules와 다른 boliderplate files들이 설치될 것이다. 그리고 src/ 디렉터리가 또한 만들어 질 것이며 아래와 같이 핵심 파일들이 있다.

src
    app.controller.spec.ts
    app.controller.ts
    app.module.ts
    app.service.ts
    main.ts

각 파일의 역할은 다음과 같다.

files description
app.controller.ts a single route가 있는 간단한 controller
app.controller.spec.ts controller를 위한 간단한 unit test를 위함
app.module.ts application의 root module
app.service.ts single method가 포함된 가장 기본적인 service
main.ts NestJs application의 진입 파일(entry file)에 대한 것이다. 핵심 함수인 NestFactory를 사용하여 Nest application instance를 생성한다.

main.ts는 동기 함수, bootstrap이 포함되어 있다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Nest Application instance를 만들기 위해, NestFactory class를 사용한다. NestFactory는 application instance를 만들기 위한 여러 개의 method를 제공한다. create()는 application object를 반환한다. 이는 INestApplication interface에 따라진다. main.ts에서는 HTTP listener를 시작하여 애플리케이션이 들어오면 HTTP 요청을 대기하도록 한다.

Nest CLI에 기반된 project는 초기 프로젝트 structure를 만든다. 이는 개발자들이 각 모듈의 관습을 따르도록 장려한다.

//기본적으로, 애플리케이션이 생성되는 동안 오류가 발생하면, 애플리케이션은 코드 1과 함께 종료된다. 이 기본 동작을 변경하여 오류를 발생시키도록하고 싶다면, `abortOnError` 옵션을 비활성화하면 된다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  try {
    const app = await NestFactory.create(AppModule, { abortOnError: false });
    // abortOnError: 이 옵션을 사용하면 애플리케이션이 종료되지 않고 오류를 던진다.
    // abort: 중단하다.
    await app.listen(3000);
  } catch (error) {
    console.error('Error starting the application:', error);
  }
}
bootstrap();

Platform

Nest는 플랫폼 종속적이지 않은 프레임워크를 지향한다. Platform independence는 framework로써 NestJs는 코드의 재사용성을 높여준다. 즉, 특정 플랫폼에 종속되지 않기에, 한 번 작성한 논리적 모듈이나 컴포넌트를 다양한 애플리케이션에서 다시 사용할 수 있다. 이는 개발 생산성을 높이고, 유지보수를 용이하게 한다. 기술적으로, Nest는 adapter가 만들어지면 어떤 Node HTTP framework라도 작동한다.
두 개의 HTTP platform이 있다. expressfastify가 있다.

플랫폼 독립적
1. 운영 체제 독립성: window, macOS, Linux...
2. 서버 프레임워크 독립성: Express, Fastify 등 다양한 HTTP 서버 프레임워크와 호환되어 사용할 수 있다.
3. 클라우드 플랫폼 독립성: 애플리케이션이 AWS, Google Cloud, Azure 등 다양한 클라우드 서비스 제공업체에서
    문제없이 배포되고 실행될 수 있다. 
HTTP platform description
platform-express Express는 node를 위한 잘 알려진 web framework다. 이는 많은 테스트를 거친, 프로덕션 준비가 된 라이브러리다. 커뮤니티에서 구현한 많은 리소스를 포함하고 있다. @nestjs/platform-express package가 기본적으로 사용된다. 많은 사용자들이 Express로 서비스를 제공받고 있으며, 이를 활성화하기 위해 별도의 작업이 필요치 않다.
platform-fastify Fastify는 높은 성능과 낮은 overhead의 framework이다. 이는 높은 효율성과 속도를 제공해준다.
Express: 웹 및 모바일 애플리케이션을 위한 일련의 강력한 기능을 제공하는 간결하고 
    유연한 Node.js 웹 애플리케이션 프레임워크이다. 즉 Node.js를 사용하여 쉽게 서버를 구성할 수 있게
    만든 클래스와 라이브러리의 집합체이다.

Fastify: Node.js를 위한 빠르면서도 오버헤드가 적은 웹 프레임워크이다.

어떤 플랫폼을 사용하든지, 이는 각자의 application interface를 나타낸다. 이는 각각 NestExpressApplicationNestFastifyApplication 사용된다.

만일 NestFactory.create() method를 사용할 때, (아래와 같이) app 객체는 specifc platform에 맞는 method들을 가질 것이다. 하지만 실제로 해당 platform API에 접근하려는 경우가 아니면 타입을 지정할 필요는 없다. 즉 특정 platform의 API를 사용하고 싶다면 타입을 지정한다.

  1. 기본 사용법(타입 지정 없음)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

bootstrap();
  1. 타입 지정(Express)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.set('trust proxy', 1); // Express 고유의 메서드 사용 예시
  await app.listen(3000);
}

bootstrap();
  1. 타입 지정(Fastify)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestFastifyApplication } from '@nestjs/platform-fastify';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(AppModule);
  app.register(require('fastify-cors')); // Fastify 고유의 메서드 사용 예시
  await app.listen(3000);
}

bootstrap();

Running the application

설치 과정이 완료되면, 다음 명령어를 운영 체제의 명령 프롬프트에서 실행하여 애플리케이션이 들어오는 HTTP 요청을 대기하도록 시작할 수 있다.

npm run start

<팁!>
만일 process의 build 속도(x20 times faster build)를 높이고자한다면, SWC builder 를 사용할 수 있다. -b swc flag를 start script에 추가하면된다. npm run start -- -b swc

위의 command는 HTTP server가 src/main.ts에 정의된 port에서 대기하도록 한다. application을 running하고 브라우저에서 'http://localhost:3000/' 접속하면 'Hello word!' 메시지를 볼 수 있다.

src의 파일을 바꿀 때 변경사항을 바로바로 확인하고 싶다면 아래의 command로 시작한다.

$ npm run start:dev

이 command는 file의 변경 사항을 자동으로 recompile하고 server를 reloading한다.

Linting and formatting

CLI는 개발함에 있어 최고의 여건을 마련해준다.
생성된 Nest projects linterformatter의 code가 함께 설치된다. 이는 각각 eslintprettier이다.

code linter: 코드의 일관성과 스타일을 유지하고, 잠재적인 오류를 발견하기 위해 코드 품질을 검사하는 도구이다. 
    Nest 프로젝트는 eslint가 사용된다.
code formatter: 코드의 형식을 자동으로 맞춰주는 도구이다. Nest 프로젝트에서는 prettier가 사용된다.

안정성(stability)와 확장성(extensibility)을 보장해주기 위해, eslintprettier package가 사용된다.
이는 공식적인 extenstions을 통해 정돈된 IDE 완성을 시켜준다.

headless 환경에서, IDE가 필요 없는 환경에서는 바로 사용할 수 있는 npm 스크립트를 제공한다.

# Lint and autofix with eslint
$ npm run lint

# Format with prettier
$ npm run format
  • headless environment: IDE(통합 개발 환경)을 사용하지 않고, 주로 명령줄에서 작업하는 환경을 의미한다.
  • npm script: package.json 파일에 정의된 명령어로, 프로젝트의 빌드, 테스트, lint 등의 작업을 자동화하는 데 사용된다.
{
  "scripts": {
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:prod": "node dist/main",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  }
}

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Exception filters  (0) 2024.06.05
Middleware  (1) 2024.06.04
Modules  (1) 2024.06.04
Providers  (0) 2024.06.04
Controllers  (0) 2024.06.03

Introduction

Nest는 효율적이고 확장가능한 server-side application을 building하기 위한 프레임워크이다.
또한 Javascript, TypeScript로 작성하는 하며 OOP(Object Oriented Programming), FP(Functional Programming), FRP(Functional Reactive Programming)적 요소를 결합하였다.

client-side: 클라이언트측(브라우저)에서 수행하는 처리를 뜻함
    - HTML, JavaScript, CSS, Ajax, JQuery ...
server-side: 서버 측에서 수행하는 처리를 뜻함
    - PHP, JSP, ASP, Ruby, Python ...

OOP(Object Oriented Programming):  객체 지향적인  프로그래밍, 즉, C언어같은 절차지향적인 프로그래밍이 아닌 객체의 
    관점에서 프로그래밍을 한다는 것
FP(Functional Programming): 모든 것을 순수 함수로 나누어 문제를 해결하는 기법, 작은 문제를 해결하기 위한 함수를 작성하여
    가독성을 높이고 유지보수를 용이하게 해준다.
RP(Reactive Programming): 데이터의 흐름과 변경사항의 전파에 중점을 둔 선언적 프로그래밍 패러다임
    e.g. 엑셀에서 C1 = A1 + B1 라고 선언적으로 정의한다. 이 때 C1의 값은 A1과 B1의 값에 따라 즉시 반영되어 변경된다.
FRP(Functional Reactive Programming): 비동기적인 데이터 처리를 간단한 함수를 통해 수행하는 프로그래밍    

좀 더 상세하게, Nest는 강력한 HTTP 서버 프레임워크를 제공한다. default로 Express와 그리고 Fastify가 있다.

Express: Node.js 환경에서 웹 애플리케이션과 API를 구축하기 위해 널리 사용되는 웹 프레임워크이다.
Fastify: 마찬가지로 Fastify도 Node.js 기반의 웹 프레임워크로, 웹 애플리케이션과 API 구축에 사용된다. 비교적 새로운 프레임워크이다.

Nest는 Node.js 프레임워크(Express/Fastify)를 통해 높은 수준의 추상화를 제공한다. 또한 Nest의 API들은 바로 개발자에게 보여준다. 개발자들은 Nest에서 사용가능한 수많은 yarn, npm 따위를 통해 많은 모듈들을 선택할수 있는 선택권을 가진다.

Philosophy

최근 몇년사이에, Node.js 덕분에 JavaScript는 front와 backend application에서 "lingua franca"가 되었다. Node.js 덕분에 Angular, React, Vue와 같은 프레임워크가 생겼고 이를 통해 frontend Application들은 생상성, 창의성, 속도, testable, 그리고 확장가능해 졌다. 하지만 다양한 Node와 server-side를 위한 JavaScript 라이브러리, 도구들은 존재하지만 소프트웨어 아키텍처와 관련된 주요 문제들을 해결해 주진 못했다.

Nest는 out-of-the-box(별도의 설치가 필요 없는) application architecture을 제공한다. 이는 개발자들에게 testable하고 확장가능하고 또 loosely=coupled하고 마지막으로 쉽게 maintainable한 application을 제공한다. Angular에 의해 영감을 받은 architecture이다.

Installation

시작을 위해서는 Nest CLI을 발판삼아 project를 시작하거나, 혹은 이미 진행되고 있는 project를 clone하는 수가 있다.

$ npm i -g @nestjs/cli
$ nest new project-name

위의 코드를 작성하면 새로운 project를 위한 directory를 만들고 초기 base structure를 위한 Nest files, modules들이 채워진다.

만일 저 엄격한 TypeScript를 만들고자 한다면 --strictnest new에 추가한다.
strict을 추가한다면 tsconfig.json에 몇 가지 설정이 바뀐다.

// 설정 전
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
// 설정 후
    "strictNullChecks": true,
    "noImplicitAny": true,
    "strictBindCallApply": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true
  1. strictNullCheck: 변수가 null 또는 undefined 값을 가질 수 있는지 여부를 엄격하게 검사하도록 한다.
    이 옵션을 활성화하면,모든 변수와 속성이 null이나 undefined 값을 가질 수 있는 지 명시적으로 선언해야 한다.
    그렇지 않으면 컴파일 오류가 발생한다.
  2. noImplicitAny: 변수나 함수의 매개변수가 암시적으로 'any' 타입을 가지지 않도록 강제하는 옵션. 이 옵션을 활성화하면, 타입을 명시하지 않은 변수나 함수 매개변수에 대해 TS가 암시적으로 any 타입을 할당하지 못하도록 한다.
  3. strictBindCallApply: Function.prototype.bind, Function.prototype.call, Function.prototype.apply 메서드 사용 시 타입 검사를 엄격하게 수행하도록 강제한다. 이 옵션을 활성화하면, 메서드를 사용할 때 인수의 타입이 정확히 일치하지 않으면 컴파일 오류가 발생한다. 즉 'bind', 'call', 'apply' 메서드를 사용할 때 정확한 타입 검사가 수행되어 더 안전한 코드를 작성할 수 있다.
// strictBindCallApply 비활성화
function add(a: number, b: number): number {
  return a + b;
}

const boundAdd = add.bind(null, '1'); // 'a'는 암시적으로 'any' 타입을 가짐
console.log(boundAdd(2)); // 정상 동작 (하지만 타입 안전성이 없음)

const result = add.call(null, '1', '2'); // 'a'와 'b'는 암시적으로 'any' 타입을 가짐
console.log(result); // 정상 동작 (하지만 타입 안전성이 없음)

// strictBindCallApply 활성화
function add(a: number, b: number): number {
  return a + b;
}

const boundAdd = add.bind(null, '1'); // 컴파일 오류: 'string' 타입은 'number' 타입에 할당할 수 없습니다.
console.log(boundAdd(2)); // 오류 발생

const result = add.call(null, '1', '2'); // 컴파일 오류: 'string' 타입은 'number' 타입에 할당할 수 없습니다.
console.log(result); // 오류 발생

// 수정된 코드
const boundAddCorrect = add.bind(null, 1); // 정상 동작
console.log(boundAddCorrect(2)); // 정상 동작

const resultCorrect = add.call(null, 1, 2); // 정상 동작
console.log(resultCorrect); // 정상 동작
  1. forceConsistentCasingInFileNames: TS의 컴파일러 옵션 중 하나로, 파일 이름의 대소문자 일관성을 강제하는 옵션이다. 이 옵션을 활성화하면, 파일 시스템이 대소문자를 구분하지 않더라도 파일 이름의 대소문자가 일관되지 않으면 컴파일 오류가 발생한다.
// 파일 이름이 MyComponent.ts인 경우.

// 비활성화
import { MyComponent } from './myComponent'; // 오류 없음 (하지만 대소문자 일관성 없음)

// 활성화
import { MyComponent } from './myComponent'; // 컴파일 오류: 대소문자 불일치

// 수정된 코드
import { MyComponent } from './MyComponent'; // 정상 동작 (대소문자 일관성 유지)
  1. noFallThroughtCasesInSwitch: TS의 컴파일러 옵션 중 하나로, switch 문에서 의도하지 않은 경우 값의 누락(fallthrough)을 방지하도록 강제하는 옵션이다. 이 옵션을 활성화하면 'switch'문에서 'case' 블록이 'break'문을 포함하지 않으면 컴퍼일 오류가 발생한다. 이는 코드의 가독성과 안전성을 높이고, 의도하지 않은 논리 오류를 방지하는데 도움이 된다.
npm i -g @nestjs/cli
에서 '-g'는 'global'을 의미한다. 이는 해당 패키지를 전역(global)로 설치하겠다는 뜻이다.
전역으로 설치된 패키지는 시스템 전체에서 접근할 수 있으며, 모든 프로젝트에서 사용할 수 있다.

Alternatives

다른방법으로, Git을 통해 TypeScript기반의 nest를 설치하고자 한다면

$ git clone https://github.com/nestjs/typescript-starter.git project
$ cd project
$ npm install
$ npm run start

참고로, 만일 git history없이 repo를 clone하고자 한다면, degit을 이용할 수 있다.
또한 TS가 아닌 JS로 시작하고자 한다면, typescript-starter대신 javascrip-starter.git을 쓸 수 있다.

마지막으로 the core와 supporting files을 포함한 project로 시작하고자 한다면 다음과 같다.
이 경우에, project의 boilerplace files를 만들 책임이 있다.

npm install @nestjs/core @nestjs/common rxjs reflect-metadata

1. 호스팅이란?

호스트이란 서버 컴퓨터의 전체 또는 일정 공간을 이용할 수 있도록 임대해 주는 서비스
인터넷 비즈니스를 시작하기 위해서는 기본적으로 홈페이지의 글/이미지/동영상 등 정보가 저장될 수 있는 '서버' 공간이 필요하다.
개인이나 기업에서 서버를 직접 보유하고 관리하는 것은 비용과 시간이 많이 들기에, '호스팅'을 이용하면 서버를 일정기간 임대하고 관리받을 수 있어 쉽게 비즈니스를 시작할 수 있다.

2. 호스팅의 구분

기본적으로 "호스팅""웹 호스팅"은 동일 용어로 간주한다.

2.1 공유 호스팅(Shared Hosting)

공유 호스팅은 여러 웹 사이트가 하나의 물리적인 서버를 공유하는 형태이다.
트래픽 양이 제한된, 새로 개설된 작은 규모의 웹사이트라면 공유 호스팅으로 시작하는 것이 좋다.
이는 가장 저렴한 호스팅 유형에 속하며, 서버 및 인프라 구축이 필요가 없다는 장점이 있다.


단점으로는 서버의 일부분만 사용하기에 사용량이 제한되고 서버 관리 권한이 없다.
또한 갑작스럽게 트래픽이 급증하는 경우에는 공유서버가 적절히 대처하지 못한다.
그렇기에 휴일에 트래픽이 크게 증가하는 온라인 쇼핑몰 사이트의 경우에는 공유 호스팅은 적절하지 못하다.
이런 경우에는 비즈니스에 영향을 주지 않고 더 많은 양의 트래픽을 처리할 수 있는 유형이 필요하다.
또한 서버 통제에 대한 권한, 리소스의 부족과 같은 문제들이 발생한다.


용량/트래픽 제한이라는 형태만 있는 채로 동일한 리소스를 나눠 쓰고 있기에
같은 서버를 공유하고 있는 다른 웹사이트에 트래픽이 몰리는 경우에는 서버 다운 타임(이유 없는 503 에러)의 영향을 받을 수 있으므로
자신의 웹사이트 뿐만이 아니라 비즈니스에도 영향을 미친다.

2.2 전용 호스팅(Dedicated Hosting)


공유 호스팅의 경우 서버중 '일부'만 빌리는 형태라면, 전용 호스팅은 서버 하나를 통째로 구매하는 형태이다.
서버 운영에 필요한 인프라와 기술력까지 제공받을 수 있는 서비스이다.


앞서 언급한 대로 공유 호스팅은 저렴한 가격으로 서버 및 인프라의 구축이 필요 없지만 서버의 일부분만 사용하기에
사용량이 제한되고 서버 관리 권한이 없다. 그렇기에 소규모 웹사이트에 주로 사용된다.


허나 전용 호스팅은 서버 관리에 대한 직접권한을 갖고 서버를 단독으로 사용하기에 보안상으로도 유리하지만
초기 구축단계에서 웹호스팅에 비해 시간과 비용이 많이 든다는 단점이 있다.


전용 호스팅은 주로 회사의 인트라넷, 대형 쇼핑몰 등 고정적으로 대용량 트래픽과 DB가 많이 사용되는 곳에 사용된다.

2.3 VPS(Virtual Private Server)


각자 물리 서버 컴퓨터의 자원을 공유하는 "공유 호스팅"과 달리 VPS물리적인 호스팅 서버를 일정 단위로 공간을 나눠 파는 것이다.

물리적인 하나의 서버를 나눠쓴다는 점에서 공유호스팅과 비슷하나, 위 이미지처럼 메모리와 같은 리소스를 애초에 배당 받기에
다른 인원의 트래픽 따위로 인한 피해를 보는 상황이 발생하지 않는다.


또, 물리적인 서버 컴퓨터가 아닐 뿐 가상으로 생겨난 하나의 서버 컴퓨터이기에 모든 설정을 바꿀 수 있는 루트 권한이 있다.
이 있고 OS선택도 가능하기에 리눅스 가상 서버 구축을 해도 되고 윈도우 가상 서버 구축도 가능하다.


단점으로 직접 통제해야하기에 서버에 많이 사용하는 리눅스에 대해서 사용할 줄 알거나 배워야 한다는 점이 있다.
또한 다른 VPS 사용자를 타깃으로 하는 DDoS 공격에 대해 동시에 영향을 받을 수도 있다.

2.4 클라우드 호스팅(Cloud Hosting)

클라우드 호스팅이란 연결된 가상 및 물리적 클라우드 서버의 네트워크에서 애플리케이션이나 웹 사이트를 호스팅하는 것을 말한다. 대표적으로 아마존 AWS, 구글의 클라우드 플랫폼 등 다양한 서비스가 존재한다.


가상 서버의 리소스를 실시간으로 유연하게 확장 및 축소가 가능하다는 점이 가상 서버 호스팅과 다른 클라우드의 큰 차이점이다.


클릭 몇 번으로 10분 안에 서버 생성하고 관리할 수 있고, 트래픽의 변동에도 유연하게 대처할 수 있기 때문에 일시적인 이벤트나 인프라가 유동적인 곳에 사용하기 편리하다는 장점이 있다.


단점으로는 트래픽이 폭증할 경우 종량제 요금으로 인한 비용 상승의 가능성이 있다. 클라우드 호스팅의 변화로 단점이 사라지고 있는 추세이다.

2.5 코로케이션

코로케이션은 고객의 서버를 내부에서 관리하지 않고 데이터 센서에서 직접 위탁 받아 초고속 인터넷 백본망에서 고객의 서버와 통신장비를 직접 연결하고 관리해주는 서비스이다. IDC 공간을 임대해서 그 안에 고객이 소유하고 있던 서버를 입주시키고 위탁 관리를 맡기는 형태다.


IDC란 서버가 입주하기에 온도와 습도를 적절하게 갖춘 쾌적한 환경과 빠른 네트워크 환경을 갖추고 있는 서버보관센터와 같은 서비스를 받쳐주는 보조 기능이다.

참조

https://velog.io/@dreamjh/%ED%98%B8%EC%8A%A4%ED%8C%85%EC%9D%B4%EB%9E%80
https://namaniflow.tistory.com/72#2.2.%20Virtual%20Private%20Server%20(VPS)
https://gentlysallim.com/vps-%ED%98%B8%EC%8A%A4%ED%8C%85%EC%9D%B4%EB%9E%80-%EC%9B%B9%ED%98%B8%EC%8A%A4%ED%8C%85%EC%9D%B4%EB%9E%80-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC/
https://m.post.naver.com/viewer/postView.naver?volumeNo=31780520&memberNo=2521903
https://modoo-game-nam.tistory.com/entry/%EC%BD%94%EB%A1%9C%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%B4%EB%9E%80-%EA%B7%B8-%EB%9C%BB%EA%B3%BC-%ED%98%B8%EC%8A%A4%ED%8C%85-%EC%B0%A8%EC%9D%B4%EC%A0%90-%ED%95%9C%EB%B2%88%EC%97%90

DNS(Domain Name System) 란?

도메인 네임 시스템(Domain Name System, DNS)은 호스트의 도메인네임(www.example.com)을 네트워크주소(291.168.1.0)로 변환하거나, 그 반대의 역할을 수행하는 시스템이다.

서비스 도메인 주소 IP 주소
다음(Daum) daum.net 203.133.167.81
네이버(Naver) naver.com 223.130.200.104
구글(Google) google.com 142.250.207.14

아래에 자세히 설명하겟지만 도메인네임으로 IP주소를 찾는 과정은 다음과 같다.
1. 도메인 주소 exam.com을 브라우저에 입력하게 되면, 도메인 주소들을 가지고 있는 네임서버(DNS 서버)에 접속
2. 네임서버에 접속한 도메인(exam.com)과 연결된 IP 정보를 확인하고, IP를 사용자 PC에 전달
3. 사용자 PC는 전달받은 서버의 IP주소로 접속
4. 서버의 IP로 연결된 브라우저에 서버의 내용(홈페이지)을 출력

DNS 작동 원리

클라이언트가 도메인명을 브라우저에 검색하면, 먼저 도메인 정보가 저장된 네임 서버(DNS 서버)로 가서 도메인과 일치하는 IP주소로 가라고 지시하게 되고, 다시 그 IP주소로 접속하게 되면 홈페이지가 열린다.


이 때 도메인 & IP정보를 얻는 과정이 약간 복잡하다.
전세계에는 도메인 수가 너무나도 많기에 DNS 서버 종류를 계층화해서 단계적으로 처리한다.

DNS 동작 순서

  1. 웹 브라우저에 www.naver.com 을 입력하면 먼저 PC에 저장된 Local DNS(기지국 DNS 서버)에게 'www.naver.com'이라는 hostname에 대한 IP 주소를 요청한다. Local DNS에는 "www.naver.com의 IP 주소"가 있을 수도 없을 수도 있다.
    • 만일 과거 접속했었다면 Local DNS에 접속정보가 캐싱되어 있어, 바로 PC에 IP 주소를 주고 끝난다.
Local DNS(기지국 DNS 서버):
    기본적으로 인터넷을 사용하기 위해서 IP를 할당해주는 통신사(KT, SK, LG 등...)에 등록하게 된다.
    컴퓨터의 LAN선을 통해 인터넷이 연결되면, 가입했던 각 통신사의 기지국 DNS 서버가 등록되게 된다.
    그러니까 KT를 사용하는 집이면 KT DNS이 되고, SK통신사 사용하는 집이면 SK DNS가 자동으로 셋팅된다.
  1. Local DNS는 이제 'www.naver.com의 IP주소"를 찾아내기 위해 다른 DNS 서버들과 통신(DNS 쿼리)을 시작한다.
    먼저 Root DNS 서버에게 "www.naver.com"의 IP 주소를 요청한다.
Root DNS(루트 네임서버):
    Root DNS는 인터넷의 도메인 네임 시스템의 루트 존이다.
    ICANN이 직접 관리하는 서버로, TLD DNS 서버 IP들을 저장해두고 안내하는 역할을 한다.
    전세계에 961개의 루트 DNS가 운영되고 있다.
  1. Root DNS 서버는 "www.navercom"의 IP주소를 찾을 수 없어 Local DNS 서버에게 "www.naver.com의 IP 주소를 찾을 수 없다고 다른 DNS 서버(TLD DNS 서버)에게 물어봐"라고 응답을 한다.

  2. 이제 Local DNS 서버는 com 도메인을 관리하는 TLD DNS 서버(최상위 도메인 서버)에 다시 www.naver.com에 대한 IP주소를 요청한다.

TLD(Top-Level Domain, 최상위 도메인) DNS Server란?
    TLD는 도메인 등록 기관(Registry)이 관리하는 서버로, 도메인 네임의 가장 마지막 부분을 말한다.
    e.g. .com, .co.kr
    또한 Authoritative DNS 서버 주소를 저장해두고 안내하는 역할을 한다.
  1. com 도메인을 관리하는 DNS 서버에도 해당 정보가 없으면, Local DNS 서버에게 "www.naver.com의 IP주소를 찾을 수 없어. 다른 DNS 서버(Authoritative DNS 서버)에게 물어봐"라고 응답한다.

  2. Local DNS 서버는 naver.com DNS 서버(Authoritative DNS 서버)에게 다시 www.naver.com의 IP 주소를 요청한다.

Authoritative DNS Server
    실제 개인 도메인과 IP 주소의 관계가 기록/저장/변경되는 서버.
    그래서 권한의 의미인 Authoritative가 붙는다.
    일반적으로 도메인/호스팅 업체의 '네임서버'를 말하지만, 개인이나 회사 DNS 서버 구축을한 경우에도 여기에 해당하게 된다.
  1. naver.com DNS 서버에는 "www.naver.com의 IP주소"가 있다. 그래서 Local DNS 서버에게 IP주소 222.122.195.6의 응답을 한다.

  2. 이를 수신한 Local DNS는 www.naver.com의 IP주소를 캐싱을 하고 이후 다른 요청이 있을 시 응답할 수 있도록 IP주소 정보를 단말(PC)에 전달해준다.

이렇게 Root DNS -> TLD DNS -> Authoritative DNS의 과정 즉, Local DNS가 여러 DNS에 차례대로 
요청하여 그 답을 찾는 과정을 "재귀적 쿼리(Recursive Query)"라고 부른다.

DNS 서버 종류

1. 기지국 DNS 서버

인터넷을 설치시 각각 통신사가 있다. 그리고 각각의 통신사마다 DNS 서버가 존재한다.

2. Root DNS 서버

Root DNS는 최상위 DNS서버로 해당 DNS부터 시작해서 아래 딸린 node DNS 서버에게로 차례차례 물어보게 되는 구조로 짜여져 있다.

트리구조로 되어 있기에 모든 DNS 서버들은 이 Root DNS Server의 주소를 기본적으로 갖고 있다.
그래서 모르는 Domain Name이 온다면 가장 먼저 Root DNS에게 물어보게 되는 것이다.


Root DNS Server에서의 목록에도 해당 Domain Name의 IP 정보가 없다면 다음 DNS 서버의 IP값을 리턴하는데, 이를 TLD(최상위 도메인) 서버이다.
만일 google.com라면 ".com"을 관리하는 TLD 서버에게 물어보라고 정보를 주는 것이다.

3. TLD 서버(Top-Level Domain, 최상위 도메인 서버)

이 루트도메인 바로 아래단계에 있는 것을 1단계 도메인이라고 하며 이를 TLD(최상위 도메인)이라고 한다.
TLD(최상위 도메인은 국가명을 나타내는 국가 최상위 도메인과 일반적으로 사용되는 일반 최상위 도메인으로 구분된다.


도메인을 구입할 경우 1단계의 도메인 중에 하나를 선태갛고 원하는 도메인명을 지정하여 등록한다.

.biz : 사업 
.com : 영리 목적의 기업이나 단체 
.co.국가로 쓰기도 한다.(co.kr 등) 
.edu : 미국의 4년제 이상 교육기관
.info : 정보 관련 
.jobs : 취업 관련 사이트 
.name : 개인 사용자 
.net : 네트워크를 관리하는 기관 
.org : 비영리 기관

4. Second-level DNS 서버(2차 도메인)

Root DNS 서버에서 return한 TLD 서버주소기지국 DNS서버에서 받아서 다시 TLD 서버에 요청을 했었다.
그리고 TLD 서버에서는 Second-level DNS 서버를 return 해준다.


만일 naver.com이나 google.com을 요청했다면, TLD 서버에서 .com을 파악하고 그 앞에 달린 문자열을 보고 네이버나 구글 서버에 요청을 하는 것이다.


그렇게 요청 받은 Second DNS 서버는 자체적으로 sub 도메인 서버로 또 넘기게 된다.

5. Sub DNS 서버(최하위 서버)

서브 도메인 서버는 www. dev. mail. cafe. 등등을 구분하는 최하위 서버를 말한다.
naver 서버라도 그 안에서 네이버 홈, 메일, 블로그, 카페 등 여러 서비스가 있다. 이 서비스들을 구분하는 도메인 네임이라고 보면 된다.

정리

예를 들어 www.naver.com을 접속한다고 가정하자.

  1. 사용자 브라우저: www.naver.com에 접속 시도
  2. 로컬 DNS 캐시 확인: 캐시에 정보가 없으면 DNS 서버로 쿼리 전송
  3. Root DNS 서버: '.com' TLD 서버 주소 반환
  4. TLD DNS 서버: naver.com의 2차 DNS 서버 주소 반환
  5. 2차 DNS 서버: www.naver.com에 대한 IP주소 또는 서브 도메인 서버 주소 반환.
  6. 서브 도메인 서버: (필요한 경우)최종 IP주소 반환

DNS 서버는 다음과 같은 구조로 되어 있다.

DNS 문자열 구조

도메인 URI는 다음과 같이 구성되어 있다.
blog.naver.com. 에서 "blog"는 sub, "naver"은 Second-level, "com"은 Top-level, "."은 Root이다.


모든 Computer들은 Root domain DNS server의 IP 주소는 알고 있다.

  • Root Domain을 담당하는 "DNS서버"에서는 TLD(Top-Level Domain)을 담당하는 서버목록과 IP를,
  • TLD(Top-Level Domain)을 담당하는 DNS 서버는 Second-Level Domain을 담당하는 서버 목록과 IP를,
  • Second-Level Domain을 담당하는 DNS 서버는 Sub Domain을 담당하는 서버 목록과 IP를 알고 있는 것이고,

결국, blog.naver.com의 IP주소는 Sub domain을 전담하고 있는 DNS 서버가 알고 있는 것이다.

DNS Cache

만일 위의 과정을 거쳐 www.gigigi.com의 IP 주소를 성공적으로 받았다고 하자.
몇 분후 다시 "www.naver.com"에 방문하려고 한다면, 또 다시 위의 같은 복잡한 과정을 반복해서 IP 주소를 받아오지 않는다.


그렇기에, PC에는 DNS Cache라는 Cache를 활용해 Cache안에 자주쓰는 Domain Name 주소를 저장해 놓는다.

출처

https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-DNS-%EA%B0%9C%EB%85%90-%EB%8F%99%EC%9E%91-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4-%E2%98%85-%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EB%A6%AC

'인터넷 기본 지식' 카테고리의 다른 글

#8. LSP  (2) 2024.06.10
#7. 프로그래밍 패러다임  (1) 2024.06.09
#5. 웹 브라우저는 어떻게 작동하는가?  (0) 2024.05.31
#4 WAS, 웹 서버  (0) 2024.05.31
#3 HTTP/HTTPS  (0) 2024.05.31

브라우저란?

웹 브라우저는 동기(Synchronous)적으로 (HTML + CSS), Javascript 언어를 해석하여 내용을 화면에 보여주는 응용 소프트웨어이다.

웹 브라우저가 웹 서버에 필요한 자원(웹 페이지)를 요청하면 서버는 응답하고 웹 브라우저는 이를 해석한 후 사용자(Client)에게 보여준다.
보통 자원은 HTML문서지만, PDF, 이미지 등 다양한 형태일 수 있다.

브라우저의 구조

1. 사용자 인터페이스(UserInterface)

사용자가 접근할 수 있는 영역. URI를 입력할 수 있는 주소 표시줄, 이전/다음 버튼, 북마크 메뉴, 새로 고침 버튼, 현재 문서의 로드를 중단할 수 있는 정지 버튼, 홈 버튼 등 요청한 페이지를 보여주는 창 등의 모든 부분

2. 브라우저 엔진

사용자 인터페이스와 렌더링 엔진 사이에서 중개자 역할을 한다.
사용자 인터페이스의 주소 입력창에 github.com 을 입력했다고 하자. 그 다음에는 브라우저 엔진이 작동하게 된다.
만약 자료 저장소(Data Storage)에 데이터가 없다면 입력받은 URI 값을 렌더링 엔진에 전달해준다.

  • 웹 브라우저마다 전용 브라우저 엔진을 사용한다.
    1. Gecko: 파이어 폭스
    2. Webkit: 사파리
    3. Blink: 크롬, 오페라
    4. Trident: 마이크로소프트
-moz-border-radius: 1em; // 파이어폭스 브라우저에 적용
-ms-border-radius: 2em; // 익스플로어에 적용, 보통 생략
-o-border-radius: 3em; // 오페라에 적용
-webkit-border-radius: 4em; // 구글, 사파리 브라우저에 적용

3. 자료 저장소(Data Storage)

자료 저장소를 거치지 않고 URI를 입력할 때마다 서버로 가서 데이터를 받아오면, 불필요한 메서드 통신이 발생한다.
따라서 자료 저장소로부터 자주 다운로드 받는 데이터를 저장해두고, 사용할 때마다 서버로부터 데이터를 요청(통신)할 필요 없이 바로 받아올 수 있다. 캐싱이라고도 한다.

4. 통신

서버에게 HTTP 요청을 하고, 서버로부터 응답받은 데이터(HTML, CSS, JS)를 렌더링 엔진에 전달해준다.

5. 자바스크립트 해석기

자료 저장소 또는 통신 단계를 거쳐 데이터를 응답 받은 렌더링 엔진은 javascript 해석기를 통해 javascript를 파싱하게 된다.
Chrome에서는 V8이라는 javascript 엔진을 사용한다. 이것을 통해 javascript를 파싱하게 되는 것이다.

6. UI 백엔드

select, input 등 기본적인 위젯을 그리는 인터페이스이다.

7. 렌더링 엔진

렌더링 엔진은 요청받은 내용을 브라우저 화면에 표시해주는 역할을 한다. 브라우저마다 사용되는 렌더링 엔진이 각각 다르기에 모든 브라우저가 동일한 소스를 화면에 동일하게 그려주지 아니한다. 또한 엔진마다 읽을 수 있는 코드의 버전도 다르기에 크로스 브라우징(cross browsing) 이슈가 발생한다.

크로스 브라우징(cross browsing): 웹 페이지 제작 시 모든 브라우저에서 깨지지 않고 의도한 대로 올바르게 나오게 하는 작업을 말한다. 

렌더링 엔진의 동작 과정

 

'HTML 파싱 단계'와 '렌더 트리 구축, 배치, 페인팅' 단계는 병렬적으로 일어난다. 즉 DOM 트리가 완성이 되는 순간 바로바로 렌더 트리가 구축이 되고 그려지게 된다. 따라서 사용자는 DOM 트리가 완벽하게 구축될 때까지 기다릴 필요 없이 렌더 트리가 그려짐으로 인해서 브라우저가 렌더링 될 때 위에서부터 조금씩 표시되는 것이다.

단계 설명
HTML 파싱 1. 렌더링 엔진은 HTML 문서의 구문 분석(파싱) 2. 파싱된 요소를 '콘텐츠 트리'라는 트리의 'DOM 노드'로 변환 3. 외부 CSS 파일, 스타일 요소도 같이 파싱 4. 스타일 정보, HTML 표시 규칙, javascript 파싱 결과물은 '렌더 트리'를 만드는데 사용된다.
렌더 트리 생성 HTML, CSS를 파싱해서 만들어진 '렌더 트리'에는 색상, 면적과 같은 시각적 속성을 포함하며, 이것을 정해진 순서대로 렌더링 한다.
렌더 트리 배치(레이아웃) DOM node는 배치 정보를 갖고 있다. 화면에 정해진 순서대로 배치하는 '레이아웃 단계'를 거치게 된다. 생성 과정이 끝났을 때 이루어지며 노드가 화면에 정확한 위치에 표시되는 것을 의미한다.
렌더 트리 페인팅 node 배치가 끝나면 UI 백엔드에서 렌더 트리의 각 node들을 순회하고, 페인팅 작업을 하여 그리기를 완료하게 된다.

1. DOM(Document Object Model), CSSOM(CSS Object Model) 생성 -> Parsing

  • HTML을 파싱하여 DOM 노드를 만든다. 이 DOM 노드들을 병합하여 DOM 트리를 만든다.
  • CSS를 파싱하여, CSSOM(CSS Object Model) 트리를 만들게 된다.

브라우저는 렌더링 할 문서를 HTML과 CSS로 나눠서 읽는데 각각 HTML Parser, CSS Parser가 담당한다. 이후 이들은 각각 Object Model을 만든다.

HTML 파싱과 DOM 생성 과정

  1. 서버는 브라우저로부터 요청받은 HTML 파일을 읽은 후 메모리에 저장, 그리고 메모리에 저장된 바이트(1101010101010...)를 응답한다.
  2. 브라우저는 응답받은 바이트 형태의 문서를 meta태그의 charset 속성에 지정된 인코딩 방식(UTF-8)에 따라 문자열로 반환
  3. 문자열로 변환된 HTML문서를 이번에는 문법적 의미를 갖는 코드의 최소 단위인 토큰(token)으로 분해한다.
  4. 토큰들의 내용에 따라 객체로 변환하여 각 노드들을 생성한다.(문서 노드, 요소 노드. 속성 노드, 텍스트 노드)
  5. HTML은 요소 간의 부자 관계인 중첩 관계를 갖는데, 이를 반영하여 모든 노드들을 트리 구조로 구성하여 DOM을 만든다.
CSS파싱과 CSSOM 생성 과정

렌더링 엔진은 HTML 문서를 한줄 한줄 순차적으로 파싱하여 DOM을 생성. 이때 CSS를 로드하는 link태그, 혹은 style태그를 만나면 DOM 생성을 중지한 후 CSS파싱의 결과물인 CSSOM을 생성하는 과정을 진행

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8"> // 여기까지 해석 후, 
  <link rel="stylesheet" href="style.css"> //link를 만나면 DOM생성을 중지하고 CSS파일을 서버에 요청한 후 응답받아 CSS파싱을 시작한다. 
  ...

css파싱 과정은 바이트 > 문자 > 토큰 > 노드 > CSSOM 생성 순으로 HTML의 파싱과정과 동일하다.

자바스크립트 파싱 과정

렌더링 엔진은 HTML 문서를 한 줄씩 순차적으로 파싱하다가 자바스크립트 파일을 로드하는 script 태그를 만나면 DOM 생성을 일시 중단한다.


script 태그의 src에 정의된 자바스크립트 파일을 서버에 요청하여 응답받으면 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에게 제어권을 넘긴다.


자바스크립트 파싱이 끝나면 렌더링 엔진으로 다시 제어권을 넘기고 DOM 생성을 이어나간다.


만약 생성되지 않은 DOM을 조작한다면 에러가 발생할 수 있다. 따라서 body 요소 아래에 자바스크립트를 위치시키거나 DOM 생성이 완료된 시점에 자바스크립트가 실행되도록 한다.

  1. 자바스크립트 코드를 토크나이저가 어휘 분석하여 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해하는데 이것을 토큰나이징이라 한다.
  2. parser가 토큰들을 구문분석하여 AST(Abstract Syntax Tree: 추상 구문 트리)로 파싱한다.
  3. 바이트 코드 생성기가 AST를 바이트코드로 변환한다.
  4. 인터프리터에 의해 바이트코드를 실행한다.

2. 렌더 트리 구축(Attachment)

CSSOM 트리와 DOM 트리를 결합하여, 표시해야 할 순서로 내용을 그려낼 수 있도록 하기 위해 렌더 트리를 형성한다. 이 과정을 Attachment라고 한다.


렌더 트리는 화면에 표시되는 각 노드의 위치를 계산하여 레이아웃에 사용되고 픽셀을 화면에 그리는 페인트 과정에도 사용된다.

렌더 트리의 생성을 위해 브라우저는 다음과 같은 작업을 한다.

  1. DOM 트리의 루트부터 노드 각각을 모두 탐색한다.
    • 이 때 화면에 표시되지 않는 일부 노드들(script, meta 태그 등...)은 렌더 트리에서 제외된다.
    • CSS 속성 중 'display:none' 같이 화면에서 숨겨지는 속성도 렌더 트리에서 반영되지 않는다.
  2. 화면에 표시되는 각 노드에 대해 적절하게 일치하는 CSSOM 규칙을 찾아 적용한다.
  3. 화면에 표시되는 노드를 콘텐츠 및 계산된 스타일과 함께 렌더트리로 생성된다.

3. 렌더 트리 배치(Layout of Reflow)

렌더 트리가 생성되고, 기기의 뷰포트 내에서 렌더 트리의 노드가 정확한 위치와 크기를 계산한다.
이때 모든 상대적인 값이 픽셀값으로 변환된다. CSS에 상대적인 값 %, rem, vh으로 할당된 값들은 절대적인 값인 px 단위로 변환된다.
이러한 배치 과정을 Layout 또는 Reflow라고 한다.

4. 렌더 트리 그리기(Paint)

렌더 트리의 각 노드를 화면의 실제 픽셀로 나타날 때 Painting메서드가 호출된다. Painting 과정 후 브라우저 화면에 UI가 나타나게 된다.

'인터넷 기본 지식' 카테고리의 다른 글

#7. 프로그래밍 패러다임  (1) 2024.06.09
#6. DNS(Domain Name System)  (0) 2024.05.31
#4 WAS, 웹 서버  (0) 2024.05.31
#3 HTTP/HTTPS  (0) 2024.05.31
#2 웹 페이지, 웹 사이트, 웹 서버 및 검색 엔진의 차이점  (0) 2024.05.30

웹서버와 WAS 서버의 비교

항목 웹서버 WAS서버
정의 정적인 컨텐츠(HTML, CSS, 이미지 등)를 제공하는 서버 동적인 컨텐츠(웹 애플리케이션)를 처리하고 제공하는 서버
기능 HTTP 프로토콜을 이용해 클라이언트에게 웹 페이지 제공 웹 애플리케이션 실행 및 데이터 처리, 웹 서버와 클라이언트 간의 중계 역할
주요 소프트웨어 Apache, NginX, IIS Tomcat, JBoss, WebLogic, WebSphere
     
실제 웹 서비스에서는 웹 서버와 WAS 서버가 함께 사용되는 경우가 많다.    

웹 서버

웹 브라우저 클라이언트로부터 HTTP 요청을 받아들이고 HTML 문서와 같은 웹 페이지를 반환하는 컴퓨터 프로그램

웹 서버는 사용자가 웹 브라우저에서 어떠한 페이지 요청을 하면 웹 서버에서 그 요청을 받아 정적 컨텐츠를 제공하는 서버이다. 여기서 정적 컨텐츠란 단순 HTML 문서, CSS, Javascript, 이미지, 파일 등 즉시 응답가능한 컨텐츠이다. 또한 웹 서버가 동적 컨텐츠를 요청 받으면 WAS에게 해당 요청을 넘겨주고, WAS에서 처리한 결과를 클라이언트(사용자)에게 전달해주는 역할도 한다.

대표적인 웹 서버로는 Apache가 있다.

웹 서버의 역할

웹 서버는 클라이언트가 웹 브라우저를 통해 요청한 정적 컨텐츠를 제공하는 역할을 한다. 웹 서버는 주로 HTTP 프로토콜을 사용하여 작동하며, 클라이언트가 URL을 통해 요청한 웹 페이지를 찾아 전송해준다.

e.g. 회사 홈페이지, 블로그, 뉴스 사이트 등

WAS 서버

인터넷 상에서 HTTP 프로토콜을 통해 사용자 컴퓨터나 장치에 애플리케이션을 수행해주는 미들웨어로서, 주로 동적 서버 컨텐츠를 수행하는 것으로 웹 서버와 구별이 되며, 주로 DB 서버와 같이 수행한다.

WAS는 웹 서버와 웹 컨테이너가 합쳐진 형태이다. 웹 서버 단독으로는 처리할 수 없는 DB의 조회나 다양한 로직 처리가 필요한 동적 컨텐츠를 제공한다. WAS는 JSP, Servlet 구동환경을 제공해주기 때문에 웹 컨테이너 혹은 서블릿 컨테이너라고도 불린다.

대표적인 WAS 종류로는 Tomcat이 있다.

웹 컨테이너: 웹 서버가 보낸 JSP, PHP 등의 파일을 수행한 결과를 다시 웹 서버로 보내주는 역할을 함

WAS 서버의 역할

WAS 서버는 웹 애플리케이션을 실행하여 동적 컨텐츠를 생성하고, 웹 서버와 클라이언트 간의 데이터 처리를 담당하는 역할을 한다. WAS 서버는 클라이언트의 요청에 따라 DB에서 정보를 가져오거나, 웹 애플리케이션을 실행하여 동적인 웹 페이지를 생성한 후 결과를 웹 서버에 전달한다.
웹 서버는 이를 받아 클라이언트에게 전달한다.

e.g. 온라인 쇼핑몰, 은행 인터넷 뱅킹, SNS 등

Web Service Architecture

웹 어플리케이션은 요청 처리 방식에 따라 다양한 구조를 가질 수 있다.

  1. 클라이언트(사용자) -> 웹 서버 -> DB
  2. 클라이언트(사용자) -> WAS -> DB
  3. 클라이언트(사용자) -> 웹 서버 -> WAS -> DB

Web Server와 WAS를 구분하는 이유

  • Web Server가 필요한 이유: Web Server에서는 정적 컨텐츠만 처리하도록 기능을 분배하여 서버의 부담을 줄일 수 있다.
    • 클라이언트에 정적 컨텐츠(이미지, 문서...)를 보낸다고 하자. 이미지 파일과 같은 정적인 파일들은 웹 문서(HTML문서)가 클라이언트로 보내질 때 함께 가는 것이 아니다. 클라이언트는 HTML 문서를 먼저 받고 그에 맞게 필요한 이미지 파일들을 다시 서버로 요청하면 그때서야 이미지 파일을 받아온다.
    • Web Server를 통해 정적인 파일들을 Application Server까지 가지 않고 앞단에서 빠르게 보내줄 수 있다.
  • WAS가 필요한 이유: WAS를 통해 요청에 맞는 데이터를 DB에서 가져와서 비즈니스 로직에 맞게 그때 그때 결과를 만들어서 제공함으로써 자원을 효율적으로 사용할 수 있다.
    • Web Server만을 이용한다면 사용자가 원하는 요청에 대한 결과값을 모두 미리 만들어 놓고 서비스를 해야한다. 허나 이를 수행하기 위한 자원은 절대적으로 부족하다.
    • WAS가 Web Server의 기능도 모두 수행하면 되지 않는가?
      1. 기능을 분리하여 서버 부하 방지: 정적 컨텐츠 요청까지 WAS가 처리한다면 정적 데이터 처리로 인해 부하가 커지게 되고, 동적 컨텐츠의 처리가 지연됨에 따라 수행 속도가 느려진다.
        1. 물리적으로 분리하여 보안 강화: SSL에 대한 암복호화 처리에 Web Server를 사용
        2. 여러 대의 WAS를 연결 가능
        • Load Balancing을 위해서 Web Server를 사용
        • fail over(장애 극복), fail back 처리에 유리
        • 큰 서비스의 경우 Web Server와 WAS를 분리하여, 또 여러 대의 서버를 사용하여 장애에 쉽게 대응할 수 있다.
        • 앞 단의 Web Server에서 오류가 발생한 WAS를 이용하지 못하도록 한 후 WAS를 재시작함으로써 사용자는 오류를 느끼지 못하고 이용할 수 있다.
load balancing: 애플리케이션을 지원하는 리소스 풀 전체에 네트워크 트래픽을 균등하게 배포하는 방법 

HTTP(Hyper Text Transfer Protocol)

웹 상에서 브라우저와 서버가 데이터를 주고 받을 때 사용하는 프로토콜

이름에 Hypertext가 포함되어 있으나 텍스트, 미디어 등의 데이터도 전송할 수 있다.

웹 브라우저와 웹 서버의 소통을 위해 디자인되었으며, 전통적인 클라이언트-서버 아키텍처 모델에서 클라이언트가 HTTP 메시지 양식에 맞춰 요청을 보내면, 이에 서버는 HTTP 메서지 양식에 맞춰 응답을 한다.

프로토콜: 통신 규악이라는 뜻으로 데이터를 주고 받는 방식에 대한 교칙

HTTP 동작 방식

HTTP는 서버/클라이언트 모델을 따른다. 클라이언트가 서버에 요청을 보내면 서버는 요청에 맞는 응답을 클라이언트에게 보낸다.

  1. connect: 클라이언트가 원하는 서버에 접속
  2. request: 클라이언트가 서버에게 원하는 요청을 보냄
  3. response: 서버가 요청에 대한 결과를 클라이언트에게 보내고 응답
  4. close: 응답이 끝나면 서버와 클라이언트 연결 종료(Stateless)
  5. HTTP 특징
  • HTML 통신은 클라이언트의 요청(Request) 와 그에 대한 서버의 응답(Response)으로 이루어진다.
  • 어떤 종류의 데이터라도 전송이 가능하다. HTML문서 말고도 단순 텍스트나 이미지, 오디오 등의 미디어 데이터도 전송 가능
  • TCP/IP를 이용하는 응용 프로토콜로 80번 포트를 사용한다.
    • TCP에서는 바이트 스트림(Byte Stream) 서비스를 제공한다. 즉 큰 데이터를 잘게 쪼갠 뒤 전송하는 서비스다.
    • 정확히 연결되었는지 확인하기 위해서 3 hand-shaking을 활용한다. 그렇기에 TCP는 신뢰성을 담당한다.
    • IP에서는 데이터 패킷들을 목적지에 전달한다.
    • 이 때 전달 주소를 IP주소로 아는 것이 아닌, MAC 주소로 구분한다. 둘을 같이 쓰는데 계층형인 IP주소를 가고자 하는 주소의 "방향"을 알 수 있다. 여기서 ARP(Address Resolution Protocol)를 사용한다.
  • 과거: 비연결성(Connectionless) 프로토콜이다. Connectionless는 한 가지 요청에 대한 응답을 받으면 그 연결을 끊어버리는 것을 의미한다. 이는 연결에 대한 리소스를 줄일 수 있는 장점이 있지만 같은 클라이언트에서 오는 요청도 계속 연결/해제 해야 한다는 단점이 있다. 그렇기에 클라이언트의 이전 상태를 서버가 알 수 없다.(stateless)
    (이를 해결하기 위해 cookiesession이 등장하였다.)
  • 현재: 지속연결 프로토콜로 바뀌었다. 지속 연결을 통해 서버의 부하를 줄이고 통신속도를 높였다. 여기서 리스폰을 기다리지 않고 바로 다음을 리퀘스트를 보내는 파이프라인(pipeline)도 가능하다. 허나 이는 성능향상이 미미하기에 잘 사용되지 않는다.
Hand-shaking: 두 호스트가 서로 연결할 때에 필요한 정보들을 주고받는 일련의 과정들
    1. 요청(SYN) : 내 목소리 들려? 들려? 들려? 응답할 때 까지 지정횟수 만큼 요청
    2. 응답1(ACK) : 응 들려
    3. 응답 2(ACK): 나도 들려! 이후 요청

IP주소: 컴퓨터 주소가 아니라 LAN Card에 연결되어 있는 회선(랜선)의 주소를 말한다. 즉 고정되어 있는 것이 아니라 인터넷맘에 접속할 때마다 달라진다. (사람 이름)

MAC주소: MAC주소는 LAC Card의 주소를 말한다. 네트워크 상에서 서로를 구분하기 위해서 Device 마다 할당된 물리적인 주소이다. 인터넷이 가능한 장비(PC, 휴대폰)들이 가지고 있는 물리적인 주소라고 생각할 수 있다.(사람 주민번호)

ARP(Address Resolution Protocol, ARP): 네트워크 상에서 IP주소를 물리적 네트워크 주소로 대응(bind)시키기 위해 사용되는 프로토콜이다. 즉 IP주소를 MAC 주소와 매칭시키기 위한 프로토콜이다.

HTTP 메시지(HTTP Message)

HTTP 메시지의 요청과 응답은 유사한 구조를 가진다.

  • Start line: 요청의 상태를 나타내며, 항상 첫 번째 줄에 위치한다.
  • Status line: 응답의 상태를 나타내며, 항상 첫 번째 줄에 위치한다.
  • HTTP headers: 요청을 지정하거나 메시지에 포함된 본문을 설명하는 헤더의 집합이다.
  • empty line: 헤더와 본문을 구분하는 빈 줄이다.
  • body: 요청과 관련된 데이터나 응답과 관련된 데이터 또는 문서를 포함한다.

요청(Requests)

HTTP Requests는 클라이언트가 서버에 보내는 메시지다.

Start line

  • 수행할 작업(GET, PUT, POST 등)이나 방식(HEAD, OPTIONS)을 설명하는 HTTP 메서드를 나타낸다.
  • 요청 대상(URL 또는 URI) 또는 프로토콜, 포트, 도메인의 절대 경로 등은 요청 컨텍스트에 작성되며 HTTP 메서드마다 다르게 작성된다.
  • HTTP 버전에 따라 HTTP 메서드의 구조가 달라진다. 따라서 start line에 HTTP 버전이 함께 입력된다.
URI(Unifrom Resource Identifier): 자원의 위치 뿐만 아니라 자원에 대한 고유 식별자로서 URL 의미를  포함한다.
    e.g. https://naver.com/mail?page=12
URL(Uniform Resource Locator): Resource의 정확한 위치 정보(파일의 위치)를 나타낸다.
    e.g. https://naver.com/mail

Headers

HTTP Requests의 Headers는 기본 구조를 따른다.
헤더의 이름, 콜론, 값 형태로 입력되며 값은 헤더에 따라 다르다.

  • 일반 헤더(General headers): 메시지 전체에 적용되는 헤더로 body를 통해 전송되는 데이터와는 관련이 없는 헤더이다.
  • 요청 헤더(Request headers): fetch를 통해 가져올 리소스나 클라이언트 자체에 대한 자세한 정보를 포함하는 헤더를 의미한다.
  • 표현 헤더(Representation headers): body에 담긴 리소스의 정보(컨텐츠 길이, mime 타입 등)를 포함하는 헤더이다.

Body

Body는 요청의 본문으로 HTTP 메시지 구조의 마지막에 위치한다. 모든 요청에 body가 필요한 것은 아니다.

GET, HEAD, DELETE, OPTIONS처럼 서버에 리소스를 요청하는 경우에는 본문이 필요하지 않다.
단, POST나 PUT과 같은 일부 요청에 대해서는 데이터를 업데이트하기 위해 body를 사용한다.

응답(Responses)

HTTP Responses는 클라이언트의 요청을 서버가 응답하는 것이다.

Status line

현재 프로토콜의 버전, 요청의 결과를 나타내는 상태 코드, 그리고 상태 코드에 대한 설명을 나타내는 상태 텍스트가 담겨있다.

Headers

  • 일반 헤더(General headers): 메시지 전체에 적용되는 헤더로, body를 통해 전송되는 데이터와는 관련이 없는 헤더이다.
  • 응답 헤더(Response headers): 위치 또는 서버 자체에 대한 정보(이름, 버전 등)와 같이 응답에 대한 부가적인 정보를 갖는 헤더이다. Vary, Accept-Ranges와 같은 상태 줄에 넣기에는 공간이 부족한 추가 정보를 제공한다.
  • 표현 헤더(Representation headers): body에 담긴 리소스의 정보(컨텐츠 길이, MIME 타입 등)를 포함하는 헤더이다.

body

응답의 바디에는 HTTP 메시지 구조의 마지막에 위치한다. 모든 응답에 body가 필요하지는 않다. 201, 204와 같은 상태 코드를 가지는 응답에는 본문을 필요로 하지 않는다.

HTTP Method

HTTP를 이용하여 클라이언트에서 서버로 요청을 보낼 때 어떠한 목적으로 요청을 하는 것인지 정의를 내릴 수 있다.
예를 들어 '단순 조회'를 목적으로 하는지, '데이터 수정'을 목적으로 하는지 등을 미리 정의내려서 요청을 보낼 수 있는데 이 때 HTTP Method라는 것을 사용한다.

이름 역할
GET 서버에게 데이터를 달라는 요청을 할 때 사용
HEAD GET과 같지만 서버가 응답할 때 Body없이 Header만 리턴
POST 서버에게 데이터를 전송하는 요청할 때 사용
PUT 서버에서 요청 URI의 데이터를 수정하거나 새로 추가하도록 요청할 때 사용
PATCH 서버의 데이터를 일부 수정할 때 사용
DELETE 서버에서 요청 URI의 데이터를 삭제하도록 요청할 때 사용
TRACE 클라이언트로부터 수신한 요청을 응답에 포함시켜서 전달(디버깅용)
OPTIONS 서버에게 특정 데이터가 어떤 Method를 지원하는지 알아볼 때 사용

HTTP 상태 코드

HTTP Status 코드라고 불리며, 서버가 응답을 전송할 때 같이 전송하는 코드이다.
앞 자리는 1~5의 숫자 중 하나이며 이 중 4와 5는 비정상적인 상황, 즉 오류가 있음을 의미한다.

MDN의 상태코드
MDN의 상태코드-고양이

  1. 1xx - 정보 응답
    • 100 continue: 현재 요청이 진행중이며 문제 없다는 것을 의미한다.
  2. 2xx - 성공 응답
    • 200 OK: 요청이 성공적으로 완료되었음을 의미한다.
    • 201 Created: 요청이 성공적으로 완료되었고 새로운 리소스가 생성되었음을 의미한다. 보통 POST아니면 PUT요청이 뒤에 따라온다.
  3. 3xx - 리다이렉션 메시지
    • 300 Multiple Choice: 요청에 대해 하나 이상의 응답이 가능함을 의미한다.
    • 301 Moved Permanently: 요청한 리소스의 URI가 변경되었음을 의미한다.
  4. 4xx - 클라이언트 에러 응답
    • 400 Bad Request: 잘못된 문법으로 인해 서버가 요청을 이해하지 못했음을 의미한다.
    • 401 Unauthorized: 요청을 보낸 클라이언트가 인증되지 않았음을 의미한다.
    • 403 Forbidden: 요청을 보낸 클라이언트가 리소스에 접근할 권리가 없음을 의미한다.
    • 404 Not Found: 서버가 요청받은 리소스를 찾을 수 없음을 의미한다.
    • 408 Request Timeout: 요청 중 시간이 초과되었음을 의미한다.
    • 418 I'm a Teapot: 서버가 찻주전자이기 때문에 커피 내리기를 거절했다는 의미이다.
  5. 5xx - 서버 에러 응답
    • 500 Internal Server Error: 서버에 문제가 있지만 서버가 해당 문제를 처리할 줄 모름을 의미한다.
    • 502 Bad Gateway: 서버가 게이트웨이로부터 잘못된 응답을 받았음을 의미한다.
    • 503 Service Temporarily Unavailable: 일시적으로 서버를 이용할 수 없음을 의미한다. 보통 유지보수를 위해 서버를 잠시 중단시켰거나 과부하로 인한 다운이 원인이다.
    • 504 Gateway Timeout: 서버가 게이트웨이 역할을 하고 있으며 다른 서버로부터 적시에 응답을 받지 못했음을 의미한다.

HTTPS란?

HyperText Transfer Protocol over Secure Socket Layer의 약자인 HTTPS는 HTTP에 데이터 암호화가 추가된 프로토콜이다. HTTPS는 HTTP와 다르게 443번 포트를 사용하며, 네트워크 상에서 중간에 제 3자가 정보를 볼 수 없도록 암호화를 지원하고 있다.

대칭키 암호화와 비대칭키 암호화

HTTPS에서는 대칭키 암호화 방식과 비대칭키 암호화 방식을 모두 사용하고 있다.

  • 대칭키 암호화
    • 클라이언트와 서버가 동일한 키를 사용해 암호화/복호화를 진행함
    • 키가 노출되면 매우 위험하지만 연산 속도가 빠름
  • 비대칭키 암호화
    • 1개의 쌍으로 구성된 공개키와 개인키를 암호화/복호화 하는데 사용함
    • 키가 노출되어도 비교적 안전하지만 연산 속도가 느림

비대칭키 암호화

비대칭키 암호화는 공개키/개인키 암호화 방식을 이용해 데이터를 암호화하고 있다. 공개키와 개인키는 서로를 위한 1쌍의 키이다.

  • 공개키: 모두에게 공개가능한 키
  • 개인키: 나만 가지고 알고 있어야 하는 키
    암호화를 공개키로 하느냐 개인키로 하느냐에 따라 상황이 달라진다.
  • 공개키 암호화: 공개키로 암호화를 하면 개인키로만 복화화할 수 있다. -> 개인키는 나만 가지고 있으므로, 나만 볼 수 있다.
  • 개인키 암호화: 개인키로 암호화하면 공개키로만 복호화할 수 있다. -> 공개키는 모두에게 공개되어 있으므로, 내가 인증한 정보임을 알려 신뢰성을 보장할 수 있다.

HTTPS의 동작 과정

HTTPS는 대칭키 암호화와 비대칭키 암호화를 모두 사용하여 빠른 연산 속도와 안전성을 모두 얻고 있다.
HTTPS 연결 과정(Hand-Shaking)에서는 서버와 클라이언트 간에 세션키를 교환한다. 여기서 세션키는 주고 받는 데이터를 암호화하기 위해 사용되는 대칭키이며, 데이터 간의 교환에는 빠른 연산 속도가 필요하므로 세션키는 대칭키로 만들어진다.

이 때 대칭키, 즉 세션키를 서버, 클라이언트 사이에 공유하는 과정에서 비대칭키가 사용된다.
즉, 처음 연결을 성립하여 안전하게 세션키를 공유하는 과정에서 비대칭키가 사용되는 것이고, 이후에 데이터를 교환하는 과정에서 빠른 연산 속도를 위해 대칭키가 사용되는 것이다.

실제 HTTPS 연결 과정이 성립되는 흐름은 다음과 같다.

  1. 클라이언트가 서버로 최초 연결 시도를 함
  2. 서버는 공개키(엄밀하게는 인증서)를 브라우저에게 넘겨줌
  3. 브라우저는 인증서의 유효성을 검사하고 세션키를 발급함
  4. 브라우저는 세션키를 보관하며 추가로 서버의 공개키로 세션키를 암호화하여 전송함
  5. 서버는 개인키로 암호화된 세션키를 복호화하여 세션키를 얻음
  6. 클라이언트와 서버는 동일한 세션키를 공유하므로 데이터를 전달할 때 세션키로 암호화/복호화를 진행함

HTTPS의 발급 과정

서버는 클라이언트와 세션키를 공유하기 위한 공개키를 생성해야 하는데, 일반적으로는 인증된 기관(Certificate Authority)에 공개키를 전송하여 인증서를 발급받는다.

  1. A기업은 HTTP 기반의 애플리케이션에 HTTPS를 적용하기 위해 공개키/개인키를 발급함
  2. CA 기업에게 돈을 지불하고, 공개키를 저장하는 인증서의 발급을 요청함
  3. CA 기업은 CA기업의 이름, 서버의 공개키, 서버의 정보 등을 기반으로 인증서를 생성하고, CA 기업의 개인키로 암호화하여 A기업에게 이를 제공함
  4. A기업은 클라이언트에게 암호화된 인증서를 제공함.
  5. 브라우저는 CA기업의 공개키를 미리 다운받아 갖고 있어, 암호화된 인증서를 복호화함
  6. 암호화된 인증서를 복호화하여 A기업의 공개키로 세션키를 공유함

인증서는 CA의 개인키로 암호화되었기에 신뢰성을 확보할 수 있다.
클라이언트는 A 기업의 공개키로 데이터를 암호화하였기 때문에 A기업만 복호화하여 원본의 데이터를 얻을 수 있다.
여기서 인증서에는 A 기업의 공개키가 포함되어 있으므로, A 기업의 공개키라고 봐도 무방하다.
또한 브라우저에는 인증된 CA 기관의 정보들이 사전에 등록되어 있어 인증된 CA기관의 인증서가 아닐 경우 다음과 같은 형태로 브라우저에서 보여지게 된다.

HTTP/HTTPS

HTTP는 암호화가 추가되지 않았기에 보안이 취약하고 HTTPS는 안전하게 데이터를 주고받을 수 있다. 허나 HTTPS의 경우 암호화/복호화 과정이 필요하기에 HTTPS보다 속도가 느리다. 또한 HTTPS는 인증서를 발급하고 유지하기 위한 추가 비용이 발생한다.

개인정보와 같은 민감한 데이터를 주고 받아야 한다면 HTTPS를, 노출이 되어도 괜찮은 단순한 정보 조회 등 만을 처리하고 있다면 HTTP를 이용하면 된다.

ref

https://jin-network.tistory.com/46
https://mangkyu.tistory.com/98
https://seing.tistory.com/183?category=552898
https://tibetsandfox.tistory.com/18
https://ittrue.tistory.com/192
https://jaehoney.tistory.com/290

+ Recent posts