반응형

1. Confluent 선택한 이유

- 로컬 노트북에 카프카를 깔고 싶지 않았고..

- 그렇다고 AWS에서 직접 EC2 여러대 만들어서 카프카 클러스터 만들고 싶지 않았음.. 어려우니까ㅠ

- 무엇보다 metric을 확인하면서 병렬처리의 성능을 평가해야하는데 Confluent에서는 이걸 쉽게 확인 가능했음..

- 즉, 내 능력이 부족하므로 플랫폼을 활용하는 것이 효율적이라 생각했음..!

- 가장 중요했던 건... 무료 60일간 400$ 이라는 것.

- 초급자가 카프카 배우기에 안성맞춤 플랫폼

 

 

2. 컨플루언트 튜토리얼 찾았음.. 

자바, 파이썬, Go, .NET, Node.js, C/C++, RestAPI, SpringBoot 등 다양한 언어도 된 튜토리얼 있음.

https://docs.confluent.io/platform/current/clients/index.html

 

Build Client Applications for Confluent Platform | Confluent Documentation

Programming languages you use to build applications that interact with Kafka provide a set of APIs and libraries. A Kafka client library provides functions, classes, and utilities that allow developers to create Kafka producer clients (Producers) and consu

docs.confluent.io

 

 

- 여기서 파이썬으로 된 카프카 클라이언트 문서 따라서 실습해볼 예정.

 

Apache Kafka and Python - Getting Started Tutorial

How to run a Kafka client application written in Python that produces to and consumes messages from a Kafka cluster, complete with step-by-step instructions and examples.

developer.confluent.io

 

 


 

튜토리얼 따라하기 과정

 

1. 카프카 클러스터 생성

Confluent Cloud Console에서 클러스터 하나 만든다.

 

 

2. 파이썬 프로젝트 생성 및 라이브러리 설치

mkdir kafka-python-getting-started && cd kafka-python-getting-started

# Python 3.x (Install the Kafka library)
pip install confluent-kafka

 

 

 

3. 카프카 클러스터 정보 복사

Confluent Cloud Console 에서 클러스터의 Bootstrap server 의 endpoint 복사해두기

 

 

 

 

4. 글로벌 키 생성 & config 파일 생성

 

(1) Key와 Secret 복사하기 

 

(2) config 정보 담긴 파일 만들기

getting_started.ini 파일 만들어서 아래 코드 넣기

[default]
bootstrap.servers={{ kafka.broker.server }}
security.protocol=SASL_SSL
sasl.mechanisms=PLAIN
sasl.username=< CLUSTER API KEY >
sasl.password=< CLUSTER API SECRET >

[consumer]
group.id=python_example_group_1

# 'auto.offset.reset=earliest' to start reading from the beginning of
# the topic if no committed offsets exist.
auto.offset.reset=earliest

 

(주의) 

bootstrap.servers={{ kafka.broker.server }}


# 에러코드
%3|1702203218.131|FAIL|rdkafka#producer-1| [thrd:sasl_ssl://'pkc-e82om.ap-northeast-2.aws.confluent.cloud:9092/b]: sasl_ssl://'pkc-e82om.ap-northeast-2.aws.confluent.cloud:9092/bootstrap: Failed to resolve ''pkc-e82om.ap-northeast-2.aws.confluent.cloud:9092': 알려진 호스트가 없습니다. (after 8ms in state CONNECT)

여기서 { } 중괄호 없고, 따옴표도 없고 키 값만 넣어야 함.

안그러면 인식 제대로 못해서 에러 발생

 

sasl.username=< CLUSTER API KEY >
sasl.password=< CLUSTER API SECRET >

# 에러코드
%3|1702205150.434|FAIL|rdkafka#producer-1| [thrd:sasl_ssl://pkc-e82om.ap-northeast-2.aws.confluent.cloud:9092/bo]: sasl_ssl://pkc-e82om.ap-northeast-2.aws.confluent.cloud:9092/bootstrap: SASL authentication error: Authentication failed (after 5116ms in state AUTH_REQ, 5 identical error(s) suppressed)

 

여기도 마찬가지로 < > 괄호 없고, 따옴표도 없고 키 값만 넣어야 함.

안그러면 인식 제대로 못해서 에러 발생

 

 

 

5. 토픽 생성

토픽명은 purchases

파티션 개수는 1개

 

 

6. 카프카 프로듀서 만들기

producer.py 파일 만들어서 아래 코드 저장

더보기
#!/usr/bin/env python

import sys
from random import choice
from argparse import ArgumentParser, FileType
from configparser import ConfigParser
from confluent_kafka import Producer

if __name__ == '__main__':
    # Parse the command line.
    parser = ArgumentParser()
    parser.add_argument('config_file', type=FileType('r'))
    args = parser.parse_args()

    # Parse the configuration.
    # See https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
    config_parser = ConfigParser()
    config_parser.read_file(args.config_file)
    config = dict(config_parser['default'])

    # Create Producer instance
    producer = Producer(config)

    # Optional per-message delivery callback (triggered by poll() or flush())
    # when a message has been successfully delivered or permanently
    # failed delivery (after retries).
    def delivery_callback(err, msg):
        if err:
            print('ERROR: Message failed delivery: {}'.format(err))
        else:
            print("Produced event to topic {topic}: key = {key:12} value = {value:12}".format(
                topic=msg.topic(), key=msg.key().decode('utf-8'), value=msg.value().decode('utf-8')))

    # Produce data by selecting random values from these lists.
    topic = "purchases"
    user_ids = ['eabara', 'jsmith', 'sgarcia', 'jbernard', 'htanaka', 'awalther']
    products = ['book', 'alarm clock', 't-shirts', 'gift card', 'batteries']

    count = 0
    for _ in range(10):

        user_id = choice(user_ids)
        product = choice(products)
        producer.produce(topic, product, user_id, callback=delivery_callback)
        count += 1

    # Block until the messages are sent.
    producer.poll(10000)
    producer.flush()

 

 

 

7. 카프카 컨슈머 만들기

consumer.py 파일 만들어서 아래 코드 저장

더보기
#!/usr/bin/env python

import sys
from argparse import ArgumentParser, FileType
from configparser import ConfigParser
from confluent_kafka import Consumer, OFFSET_BEGINNING

if __name__ == '__main__':
    # Parse the command line.
    parser = ArgumentParser()
    parser.add_argument('config_file', type=FileType('r'))
    parser.add_argument('--reset', action='store_true')
    args = parser.parse_args()

    # Parse the configuration.
    # See https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md
    config_parser = ConfigParser()
    config_parser.read_file(args.config_file)
    config = dict(config_parser['default'])
    config.update(config_parser['consumer'])

    # Create Consumer instance
    consumer = Consumer(config)

    # Set up a callback to handle the '--reset' flag.
    def reset_offset(consumer, partitions):
        if args.reset:
            for p in partitions:
                p.offset = OFFSET_BEGINNING
            consumer.assign(partitions)

    # Subscribe to topic
    topic = "purchases"
    consumer.subscribe([topic], on_assign=reset_offset)

    # Poll for new messages from Kafka and print them.
    try:
        while True:
            msg = consumer.poll(1.0)
            if msg is None:
                # Initial message consumption may take up to
                # `session.timeout.ms` for the consumer group to
                # rebalance and start consuming
                print("Waiting...")
            elif msg.error():
                print("ERROR: %s".format(msg.error()))
            else:
                # Extract the (optional) key and value, and print.

                print("Consumed event from topic {topic}: key = {key:12} value = {value:12}".format(
                    topic=msg.topic(), key=msg.key().decode('utf-8'), value=msg.value().decode('utf-8')))
    except KeyboardInterrupt:
        pass
    finally:
        # Leave group and commit final offsets
        consumer.close()

 

 

 

8. 카프카 프로듀서 이벤트 생성하기

 

chmod 파일 권한변경 명령어

u+x : user에게 excution(실행) 권한 부여 후 파이썬 코드 실행

chmod u+x producer.py

./producer.py getting_started.ini

 

 

 

9. 카프카 컨슈머 이벤트 생성하기

chmod 파일 권한변경 명령어

u+x : user에게 excution(실행) 권한 부여 후 파이썬 코드 실행

chmod u+x consumer.py

./consumer.py getting_started.ini

 

 

 

 

 


 

최종 결과

 

 

생성파일

 

 

출력 화면

반응형

'백엔드 > Kafka' 카테고리의 다른 글

[Kafka] 심화 (병렬처리 / 스트림즈 / 커넥트 )  (0) 2023.12.10
[Kafka] 개념  (1) 2023.12.06
반응형

컨슈머

configs : 어느 카프카 브로커에서 데이터를 가져올지 선언.

consumer.subscribe() 메서드: 어느 토픽에서 데이터를 가져올지 설정

 

> 0.5초 동안 watit 

> records 반환 ( 이 데이터는 프로듀서가 카프카에 전송한 데이터)

이를 무한루프로 반복.

 

 

consumer.assign() 메서드: 토픽의 어느 파티션에서 데이터를 가져올지 설정

 

 

파티션 개수와 컨슈머 개수의 관계성

 

토픽 내 파티션 개수는 무조건 컨슈머 개수보다 많아야 함.

여러 파티션을 가진 토픽에 대해서, 컨슈머를 병렬처리 하고 싶다면 파티션 수보다 적은 개수로 실행해야 함.

 

 

 

컨슈머에서의 병렬처리

컨슈머 그룹별로, 토픽별로 offset을 저장하기 때문에 병렬처리가 가능하다.

 

 

프로듀서에서의 병렬처리

데이터를 해키(키)를 이용해 특정 파티션에만 저장할 수 있음. 

만약 파티션 추가하면 병렬처리가 깨짐

 

 

 

 





 

 

 

1. 카프카 스트림즈

대용량, 폭발적인 성능의 실시간 데이터 처리를 위한 기능의 컴포넌트

 

2. 카프카 커넥트

반복적으로 파이프라인을 배포하고 관리하는 기능의 컴포넌트

 

커넥트Connect:

커넥터가 동작하도록 실행해주는 프로세스. 먼저 실행되어야 커넥터를 실행할 수 있음.
json 형태로 토픽과 테이블명을 지정해 rest API호출하면 파이프라인이 만들어지는 과정.

1) 단일 실행 모드 커넥트
 간단한 데이터 파이프라인, 또는 개발용으로 사용된다.

2) 분산 모드 커넥트
실제 상용할때 쓰이는 모드. 여러 프로세스를 하나의 클러스터로 묶은 것. 두개 이상의 커넥트가 하나의 클러스터로 묶이는 것. 일부 클러스터에 장애발생에도 다른 커넥트 이용해 데이터 처리가능하다.

 

 

커넥터 Connector: 

실질적으로 데이터를 처리하는 코드가 담긴 jar 패키지. 
동작/설정/실행 메서드가 들어있음. 

1) 싱크 커넥터 (sink connector)
특정 토픽에 있는 데이터를 어딘가로 sink한다. 특정 저장소(오라클, elastic search 등)으로 저장하는 역할. (컨슈머 같은 역할)

2) 소스 커넥터 (source connector)
데이터베이스로부터 데이터를 가져와서 토픽에 저장하는 역할 ( 프로듀서 같은 역할)

 

 

 

 

반응형

'백엔드 > Kafka' 카테고리의 다른 글

[Kafka] Confluent 플랫폼 이용한 카프카 실습 (with python)  (0) 2023.12.10
[Kafka] 개념  (1) 2023.12.06
반응형

 

아파치 카프카

무엇
데이터를 전송하는 source와 데이터를 받는 target이 점점 많아지면 데이터 전송 관계들이 복잡해진다.
이 복잡함을 해결하기 위해 등장한 오픈소스
장점
낮은 지연과 높은 처리량으로 효과적으로 빅데이터 처리가 가능하다. 

 

 

 

카프카 클라이언트

 

카프카 프로듀서

- 토픽에 해당하는 메시지를 생성

- 특정 토픽으로 데이터를 publish

- 처리 실패/재시도 

 

카프카

- 데이터를 담는 큐와 같은 역할을 한다고 보면 된다.
- 데이터 흐름에 있어서 fault tolerant

- 가용성으로 서버이슈 발생해도 데이터를 복구할 수 있음

카프카 컨슈머

- 토픽 내 파티션의 데이터를 가져옴. (polling)

- 파티션의 오프셋 위치를 기록 (commit)

- 컨슈머 그룹을 통해 병렬처리 가능 (파티션의 개수에 따라 컨슈머를 여러개 만들면 병렬처리가 가능해진다.)

 

 

 

토픽

데이터를 담는 공간을 토픽이라고 한다.

토픽은 여러개 생성할 수 있다.  DB의 테이블/ 파일시스템의 폴더 같은 역할을 한다. 

Producer는 토픽에 데이터를 저장하고, Consumer는 토픽에서 데이터를 가져간다. 

토픽 이름은 데이터의 특징을 반영하게 짓는 것이 유지보수에 유리하다. (click_log 이런식)

 

파티션

토픽 내에 여러개 파티션으로 구성할 수 있다.
하나의 파티션은 큐와 같이 데이터가 차곡차곡 쌓이고, Consumer는 가장 오래된 데이터부터 가져간다.  
(Consumer가 record를 가져가도 데이터는 삭제되지 않음. 새로운 Consumer 들어오면 처음부터 가져간다)

만약 파티션이 2개라면, 데이터를 저장할 때 어느 파티션에 저장할지 키를 지정할 수 있다.
파티션을 늘리면 Consumer를 늘려서 분산처리를 할 수 있다.  (파티션은 늘리는 것이 가능하지만, 줄일 수는 없다.)
파티션 데이터의 삭제는 보존기간과 보존크기를 미리 정하면 일정 기간/ 용량동안 저장되었다가 삭제될 수 있다.

 

 

브로커

카프카가 저장되어 있는 서버를 의미. 보통 3개 이상의 브로커로 구성함.

만약 브로커가 3대라면, 그 중 한대에 토픽정보가 저장된다.

복제 (Replication)
replication이 3이라면, 파티션은 원본 1대와 복제본 2대로 구성된다.  (leader partition 1, follower partition 2)
만약 브로커가 사용불가하게 되면, 리더 파티션은 못쓰게 되도 팔로워 파티션으로 복구 가능해진다.
ack는 0, 1, all  셋 중 하나를 사용할 수 있다. 
0 - 리더파티션에 데이터 전송 후, 잘 전송됐는지 확인하지는 않는다. 데이터 전달 속도 빠르지만 유실 가능성 있음.
1 - 리더파티션에 데이터 전송 후, 잘 전송됐는지 확인한다. 팔로워 파티션에 복제됐는지 확인하지 않음.
all - 리더파티션에 데이터 전송 후, 잘 전송됐는지 확인한다. 복제 잘 됐는지 응답까지 확인 (너무 느림..)
ISR (In-Sync-Replication)
원본 + 복제본 합쳐서 ISR 이라고 지칭한다.

 

 

파티셔너

파티션을 더 효과적으로 쓰기 위해 파티셔너 존재함.

데이터를 토픽의 어떤 파티션에 넣을지 결정하는 역할을 한다.

메시지의 키에 따라 특정 해시값이 결정되고, 이를 기반으로 저장될 파티션이 결정된다. 

 

 

컨슈머 랙 (consumer lag)

 

카프카 운영함에 있어 모니터링 해야하는 지표 중 하나.

프로듀서가 마지막으로 저장한 오프셋과 컨슈머가 마지막으로 읽은 오프셋의 차이

 

카프카 프로듀서 : 토픽의 파티션에 데이터를 저장하는데 오프셋 0부터 시작한다.

카프카 컨슈머 : 데이터를 오프셋0번부터 가져간다. 

 

 

컨슈머그룹이 1개이고, 만약 파티션이 2개라면 lag은 2개가 측정된다.

그중 큰 값이 records lag max 값이다.

 

 

 

카프카 버로우 (Burrow)

컨슈머 랙 모니터링 애플리케이션

 

1. 버로우 1대 이용하여, 멀티 카프카 클러스터 지원

2. 윈도우 wanrning, error

3. HTTP api를 제공한다.

 

 

 

카프카 /  레빗엠큐RabbitMQ / 레디스 큐의 차이

메시지 브로커: RedisQ, RabbitMQ

대규모 메시지 기반 미들웨어, 전송이 되고 나면 삭제된다.

 

이벤트 브로커: 카프카, AWS의 키네시스

이벤트를 보존한다. 서비스에서 나오는 이벤트를 DB에 저장하듯이 큐에 저장한다.

1. 한번 일어난 이벤트도 저장하여 "단일진실공급원"으로서 사용이 가능하다.

2. 장애가 발생했을때 그 이전부터 재처리 할 수 있음.

3. 많은 양의 스트림 데이터를 실시간으로 처리할 수 있다.

 

반응형
반응형

 


계층 구조

 

3계층 구조(3-tier Architecture)

  • Presentation Tier: 사용자 인터페이스 혹은 외부와의 통신을 담당 (nest에선 Controller)
  • Application Tier: Logic Tier라고 하기도 하고 Middle Tier라고 하기도 함. 주로 비즈니스 로직을 여기서 구현을 하며, Presentation Tier와 Data Tier사이를 연결 (nest에선 Service)
  • Data Tier: 데이터베이스에 데이터를 읽고 쓰는 역할을 담당

 

 

계층형 구조(Layered Architecture)

3계층 구조처럼 계층형 구조를 사용하는 이유는 무엇일까.

소프트웨어 개발시, 각 계층은 응집도가 높으면서다른 계층과는 낮은 결합도를 가져야 한다는 설계법칙이 있다.

복잡해 보이는 작업도 그 작업을 분리하고 작은 단위로 쪼개면 간단하게 해결할 수 있다. 계층화의 핵심은 이것이다.

단, 상위 계층은 하위 계층을 사용할 수 있지만 하위 계층은 본인의 상위 계층으로 누가 있는지 인식 못하도록 해야한다.

(Presentation 계층은 Application  계층을 알 수 있지만, Application 계층은 Presentation 계층을 인식하지 못해야만 한다)

 

 

 


의존성 주입 (DI)

 

추상화

추상화는 곧 인터페이스를 의미한다.

인터페이스를 이용하면 프로그래머는 호출하는 클래스가 무엇인지 알 필요없이,

특정 기능에만 초점을 맞춰 구현하면 된다. 따라서 클래스가 변경되거나 확장되더라도 문제가 발생하지 않게 된다.

구체적인 특정 객체에 의존하지 말고 인터페이스(추상화)에 의존하라는 것이다.

 

인터페이스 예시

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

 

제어 역전(IoC, Inversion of Control)

일반적으로 객체의 생명주기(생성, 호출, 소멸,, 등)를 제어하는 권한을 프로그래머가 갖는다.

하지만 이를 프로그래머가 직접 관리하지 않고 외부(프레임워크)에 위임하는 설계 원칙을 제어의 역전이라고 한다.

객체는 스스로가 사용할 객체를 선택할 수 없고, 외부로부터 결정된다. 따라서 코드 내에 new를 이용한 객체 생성이 없다.

 

 

의존성 Dependency

의존성이 높은 코드는 new를 통해 객체를 만드는 로직을 말한다. 

의존성을 낮추기 위해서는 외부로부터 객체를 받아 사용하도록 해야한다. (의존성 주입)

즉 객체를 직접 생성하지 않고 객체를 전달받는 형태로 구성이 되면 의존성이 낮아졌다고 한다.

 

 

의존성 주입 (DI, Dependency Injection)

객체간의 결합을 느슨하게 만들어 의존성을 낮추고, 유연하고 확장성 있는 코드를 작성하기 위한 패턴이다.

외부(프레임워크)에서 객체의 생명주기를 관리하는 개념이라고 보면 된다.

nestJS는 DI를 통해 IoC를 구현한 프레임워크이다.

 

 

 


Provider

Provider

Nest 클래스(서비스, 리포지토리, 팩토리, 헬퍼 등)는 Provider로 취급되어 다른 컨트롤러(소비자)에 종속적으로 주입되어 컨트롤러(소비자)에게 제공될 서비스이다. 즉, 개체는 서로 다양한 관계를 만들 수 있으며 개체 인스턴스를 "연결"하는 기능은 대부분 Nest 런타임 시스템에 위임한다. 

 

 

코드로 확인하기

 

1. Service 정의하기

아래 CatsService는 데이터 저장 및 검색을 담당하도록 설계되었다.

CatsService는 CatsController의 Provider로 정의하기에 좋다.

//	src/app.service.ts

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

@Injectable()		// 해당 어노테이션이 붙은 것만 provider로 쓰여질 수 있음
export class CatsService {
  private readonly cats: Cat[] = [];

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

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

 

2. Provider 등록하기

위에서 만든 서비스를 공급자로 등록하면, 컨트롤러에서 의존성 주입으로 서비스를 전달해줄 수 있다.

//	src/app.module.ts 

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],			// 여기에 Provider를 정의해야 한다.
})
export class AppModule {}

 

3. Controller에서 사용하기

app.module에서 provider로 정의된 서비스는 컨트롤러에 주입되어 사용가능해진다.

주로 생성자에서 주입되며 선언을 따로 하지 않고 곧바로 사용이 가능하다.

//	src/app.controller.ts 

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) {}	// 생성자에서 주입된 CatsService instance

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);		// 공급받은 CatsService instance를 사용할 수 있음
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();			// 공급받은 CatsService instance를 사용할 수 있음
  }
}

 

 

 

 

 

 

 

 

 

 

 

참고 링크

https://docs.nestjs.com/providers

 

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 Reac

docs.nestjs.com

https://www.wisewiredbooks.com/nestjs/overview/04-provider.html

 

프로바이더 - 쉽게 풀어 쓴 Nest.js

프로바이더는 Nest의 기본 개념입니다. 많은 기본 Nest 클래스는 서비스(Service), 레파지토리, 팩토리, 헬퍼 등등의 프로바이더로 취급될 수 있습니다. 프로바이더의 주요 아이디어는 의존성을 주입

www.wisewiredbooks.com

 

 

반응형

'백엔드 > nest.js' 카테고리의 다른 글

[nest.js] NestJS구조와 컨트롤러 패턴  (0) 2022.11.24
[nest.js] 개발환경 세팅하기  (0) 2022.11.24
반응형

컨트롤러

컨트롤러를 정의하는데 필요한 @Controller( ) 데코레이터. router( ) 역할

컨트롤러는 들어오는 요청 을 처리 하고 클라이언트에 응답 을 반환 하는 역할

 

 

 

라우팅

@Get( ) 데코레이터 http요청 메소드 데코레이터는 nest에세 http요청(get, post, put, delete)에 대한 핸들러를 생성

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

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

위 라우터는 GET요청의 /cats/profile 이다.

 

 

요청 객체

컨트롤러를 정의하는데 필요한 @Req( ) 데코레이터. 

@Req( ) request: Request,

@Body( ) Body

이렇게 값을 받아올 수 있다.

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';
  }
}
@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

 

 

상태 코드

상태 코드 는 201 인 POST 요청을 제외하고 기본적으로 항상 200 

핸들러 수준에서 데코레이터를 추가하여 이 동작을 쉽게 변경할 수 있다.

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

 

 

리디렉션

응답을 특정 URL로 리디렉션하려면 @Redirect()데코레이터 또는 라이브러리별 응답 개체를 사용하고 res.redirect()직접 호출할 수 있다.

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

 

 

경로 매개변수

@Param( )

 

@Param()메서드 매개 변수를 장식하는 데 사용되며, 경로 매개 변수를 메서드 본문 내에서 장식된 메서드 매개 변수의 속성으로 사용할 수 있다. 

 

params.id

id 참조하여 매개변수에 액세스할 수 있다.

특정 매개 변수 토큰을 데코레이터에 전달한 다음 메서드 본문에서 이름으로 경로 매개 변수를 직접 참조할 수도 있다.

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

 

 

 

 

참고)

https://docs.nestjs.com/controllers

 

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 Reac

docs.nestjs.com

 

반응형

'백엔드 > nest.js' 카테고리의 다른 글

[nest.js] 계층 구조 / DI / Provider  (0) 2022.11.25
[nest.js] 개발환경 세팅하기  (0) 2022.11.24
반응형

공식 문서 정리

- 공식 문서 : https://docs.nestjs.com/

- 공식 문서 한국어 번역 : https://docs.nestjs.kr/

 

 


 

환경 세팅하기

1. 설치

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

 

2. npm 또는 yarn  선택가능

공식문서에서 npm 명령어 위주로 설명하므로 npm을 선택할 예정

 

3. packaage.json의 script 확인해보면, 

start:dev와 start:debug - 개발할 때 주로 사용

start:prod - 프로덕션시 사용

 

4. src 폴더 내에서

app.controller.ts 단일 경로가 있는 기본 컨트롤러. Controller는 express에서 router와 같은 역할
app.controller.spec.ts 컨트롤러에 대한 단위 테스트입니다. app.controller를 테스트 하기 위한 파일
app.module.ts 응용 프로그램의 루트 모듈입니다.
app.service.ts 단일 메서드를 사용하는 기본 서비스입니다.
main.ts 핵심 기능 NestFactory을 사용하여 Nest 애플리케이션 인스턴스를 생성하는 애플리케이션의 엔트리 파일입니다.

5. teset폴더

프로그램을 테스트 하기 위한 폴더

 

 

반응형

'백엔드 > nest.js' 카테고리의 다른 글

[nest.js] 계층 구조 / DI / Provider  (0) 2022.11.25
[nest.js] NestJS구조와 컨트롤러 패턴  (0) 2022.11.24
반응형

회원가입과 로그인

기능

- 간단한 회원가입 페이지 구현 - 이메일/비밀번호 

- 이메일 형식이 올바른지 확인하기.

- 비밀번호 최소 길이 확인하기.

- 패스워드와 패스워드 확인문자가 일치하는지 확인하기.

- form 이용해 post 요청을 전송한다.

- 회원가입 처리 및 redirect

 

 

회원정보 DB에 저장

- 비밀번호 저장시, HASH 암호화하여 저장하여 보안 취약점 해결

- node.js에서 기본제공하는 crypto 모듈로 sha1, sha224m sha256 등 알고리즘 이용해 hash값 얻을 수 있다.

( hash: 문자열을 되돌릴 수 없는 방식으로 암호화하는 방법이다. )

// hash-password.js

const crypto = require('crypto');

module.exports = (password) => {
  const hash = crypto.createHash('sha1');
  hash.update(password);
  return hash.digest("hex"); // 16진수로 변환하여 반환한다.
}

 

 

회원 가입 기능 구현

- email, name, password 세가지 항목을 입력받은 뒤 post 요청이 도착.

- password는 따로 구현한 해시함수를 이용해 변환

- mongoose에서 제공하는 model.create({ }) 함수를 이용해 객체를 생성하여 DB에 저장

const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { User } = require('../models');
const hashPassword = require('../utils/hash-password');

router.post(
  '/join',
  asyncHandler(async (req, res) => {
    const { email, name, password } = req.body;
    const hashedPassword = hashPassword(password); // 비밀번호 해쉬값 만들기
    const user = await User.create({
      email,
      name,
      password: hashedPassword, //평문 비밀번호가 아닌 암호화된 비밀번호를 저장한다.
    }); // 회원 생성하기

    // 아래 코드 수정 시 오답으로 처리될 수 있습니다.
    console.log('신규 회원', user);

    res.redirect('/'); // 메일 페이지로 redirect한다.
  })
);

 

src/utils/hash-password.js

const crypto = require('crypto');

module.exports = (password) => {
  const hash = crypto.createHash('sha1');
  hash.update(password);
  return hash.digest("hex");
}

 

src/utils/async-handler.js

module.exports = (requestHandler) => {
  return async (req, res, next) => {
    try {
      await requestHandler(req, res);
    } catch (err) {
      next(err);
    }
  }
}

 


PASSPORT 

 

 

passport.js

express.js 어플리케이션에 간단하게 사용자 인증 기능을 구현하게 도와주는 패키지이다.

유저의 세션관리 및 다양한 로그인 방식을 추가할 수 있게 한다.

 

1. 폼 check (프론트엔드 쪽에서 구현)

이메일, 비밀번호 값이 들어있는지 체크. 비어있으면 굳이 비밀번호 길이 확인이나 post 요청 보내지 않아도 된다.

 

2. passport-local strategy

- new LocalStrategy 함수에 config 객체(usernameFeild와 passwordFeild)를 전달.

 

src/passport/strategies/local.js

const LocalStrategy = require('passport-local').Strategy;
const { User } = require('../../models');
const hashPassword = require('../../utils/hash-password');

const config = {
  usernameField: 'email',       // 'email' 필드 사용하도록 설정
  passwordField: 'password',    // 'password' 필드 사용하도록 설정
};

const local = new LocalStrategy(config, async (email, password, done) => {
  try {
    const user = await User.findOne({ email });
    if (!user) {
      throw new Error('회원을 찾을 수 없습니다.');
    }
    // 검색 한 유저의 비밀번호와 요청된 비밀번호의 해쉬값이 일치하는지 확인
    if (user.password !== hashPassword(password)) {
      throw new Error('비밀번호가 일치하지 않습니다.');
    }

    done (null, {
      shortId: user.shortId,
      email: user.email,
      name: user.name,
    });
  } catch (err) {
    done(err, null);
  }
});

module.exports = local;

- email 값으로 회원이 있는지 확인

- password 값으로 기존회원의 비밀번호와 일치하는지 확인 (이때 받은 평문 password를 hash함수로 변환하여 비교한다.)

 

 

3. done callback

error가 있으면 don(err, null) 전달, 아니면 사용자 정보를 전달한다.

사용자 정보를 전달할 때 user 객체 전체를 전달하면,  비밀번호 해시값 같이 중요정보까지 전달되므로 비추천한다.

done (null, user);

아래처럼 제한하여 세션에 저장되는 사용자 정보를 최소화한다. (shortId, email, name )

done (null, {
      shortId: user.shortId,
      email: user.email,
      name: user.name,
});

 

 

4. passport.use

작성한 strategy를 passport.use를 이용해 사용하도록 선언한다.

 

src/passport/index.js

const passport = require('passport');
const local = require('./strategies/local');

module.exports = () => {
  // local strategy 사용
  passport.use(local);

  passport.serializeUser((user, callback) => {
    callback(null, user);
  });

  passport.deserializeUser((obj, callback) => {
    callback(null, obj);
  });
};

세션 유저 활용하기 - serializeUser, deserializeUser

세션을 이용해 user를 사용할 때에 설정해주어야 한다. 세션에 user정보를 변환하여 저장하고 가져오는 기능을 제공한다. 

예) 회원id를 세션에 저장한 후, 사용할 때 회원정보를 DB에서 찾아서 사용한다.

이 함수들을 사용하지 않으면 passport 로그인이 동작하지 않는다.

 

 

 

5. passport.authenticate 함수

http 라우팅에 연결하면 passport가 자동으로 해당하는 strategy를 사용하는 request handler를 생성한다.

 

src/routes/auth.js

const { Router } = require('express');
const passport = require('passport');

const router = Router();

// passport local 로 authenticate 하기 (미들웨어 추가)
router.post('/', passport.authenticate('local'), (req, res, next) => {
  res.redirect('/');
});

module.exports = router;

 

6. 로그인

express-session과 passport-session()을 사용하면 passport가 로그인시 유저 정보를 세션에 저장하고 가져오는 동작을 자동으로 수행해준다. 

 

src/app.js

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const mongoose = require('mongoose');
const dayjs = require('dayjs');
const session = require('express-session');
const passport = require('passport');

const indexRouter = require('./routes');
const postsRouter = require('./routes/posts');
const authRouter = require('./routes/auth');

const loginRequired = require('./middlewares/login-required');

require('./passport')();

mongoose.connect('mongodb://localhost:27017/simple-board');

mongoose.connection.on('connected', () => {
  console.log('MongoDB Connected');
});

const app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.locals.formatDate = date => {
  return dayjs(````date````).format('YYYY-MM-DD HH:mm:ss');
};

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(
  session({
    secret: 'elice',
    resave: false,
    saveUninitialized: true,
  })
);

// passport initialize
// passport session
app.use(passport.initialize());
app.use(passport.session());

app.use('/', indexRouter);
// /posts 경로에 로그인 필수로 설정하기
app.use('/posts', loginRequired, postsRouter);
app.use('/auth', authRouter);

// catch 404 and forward to error handler
app.use((req, res, next) => {
  next(createError(404));
});

// error handler
app.use((err, req, res, next) => {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 

 

 

7. req.logout 함수

passport는 req.logout 함수를 통해 세션의 로그인 정보를 삭제하여 로그아웃 기능을 구현할 수 있다.

router.get('/logout', ... {
	req.logout();
    res.redirect('/');
});

 

 

8. login 미들웨어

로그인을 필수로 설정하고 싶은 경우, 미들웨어를 사용하여 체크할 수 있다.

function loginRequired(req, res, next) {
    if (!req.user){
        res.redirect('/');
        return;
    }
    	next();
    }

app.use('/[psts', loginRequired, postsRouter);

 

 

 

 

반응형
반응형

Async request handler

 

async를 이용할 떄 항상 try-catch를 하여 에러처리를 했어야 했다.

이를 하나의 미들웨어 형식으로 만들어, 같은 코드는 줄어들고 사용은 편리해진다.

// utils/async-handler.js

module.exports = (requestHandler) => {
  return async (req, res, next) => {
    try {
      await requestHandler(req, res);
    } catch (err) {
      next(err);
    }
  }
}

 

아래는 asyn-handler를 불러와 사용하는 코드이다.

// routes/posts.js

router.get('/', asyncHandler(async(req, res) =>{
    constposts = awaitPosts.find({});
    if(posts.length< 1) { 
    	thrownew Error('Not Found');
	}
    res.render('posts/list', { posts });
});

asyncHandler는 requestHandler를 매개변수로 갖는 함수형 미들웨어

전달된 requestHandler는 try ~ catch로 감싸져 asyncHandler내에서 실행되고,

throw 되는에러는 자동으로 오류처리미 들웨어로 전달되도록 구성됨

 


Pagination

    const page = Number(req.query.page || 1); // url 쿼리에서 page 받기, 기본값 1
    const perPage = Number(req.query.perPage || 10); // url 쿼리에서 perPage 받기, 기본값 10

    // total, posts 를 Promise.all 을 사용해 동시에 호출하기
    const [total, posts] = await Promise.all([
      Post.countDocuments({}),
      Post.find({})
        .sort({ createdAt: -1 }) // 최신순으로 정렬
        .skip(perPage * (page - 1))
        .limit(perPage),
    ]);

    const totalPage = Math.ceil(total / perPage);
    res.render('post/list', { posts, page, perPage, totalPage });

 

 

 


PM2

pm2

node.js 작업을 관리해주는 프로세스 매니저이다. node 명령어로 실행시 오류 발생이나 실행상태 관리를 할 수 없다.

pm2는 작업관리를 위한 다양한 기능을 제공한다.

- 안정적인 프로세스 실행: 오류 발생시 자동 재실행한다.

- 빠른 개발환경: 소스코드 변경시 자동 재실행

- 배포시 편리한 관리: pm2 에 모든 프로세스를 한번에 관리한다.

pm2 init simple // 또는 pm2 init
module.exports = {
    apps: [{
        name: 'simple-board',
        script: './bin/www',
        watch: '.',
        ignore_watch: 'views',
    }],
}
pm2 start // pm2 데몬으로 실행해준다.

 

프로젝트 관리 모니터링

pm2 status
pm2 monit
반응형

+ Recent posts