Lombok (롬복)@(annotation, 어노테이션) 사용하여 코드를 줄여주고 간소화해주는 인기 있고 널리 사용되는 라이브러리

 

  1. 코드 간결성: Getter, Setter, toString 등과 같은 반복적인 코드를 자동으로 생성하여 코드의 양을 줄여준다. 이로 인해 코드가 더 간결해지고 가독성이 좋아진다
  2. 생산성: 반복적인 작업을 줄일 수 있으므로, 생산성을 향상시킨다
  3. 유지 보수성: 코드 중복을 줄여주고 일관된 코드 스타일을 유지하도록 도와주기 때문에, 유지 보수가 더 쉬워진다

Annotation 종류

 

@Getter / @Setter

import lombok.Getter;
import lombok.Setter;

@Getter
public class User {
	@Setter String id;
	@Getter @Setter String name;
	int age;
}

class 전체에 부여할 수 있고, 컬럼에도 사용이 가능하다

 


@ToString

import lombok.ToString;

@ToString
public class User {
	String id;
	String name;
	int age;
}

클래스의 toString() 메서드를 자동으로 생성하여 객체의 문자열을 만든다

 


@EqualsAndHashCode

import lombok.EqualsAndHashCode;

@EqualsAndHashCode
public class User {
	String id;
	String name;
	int age;
}

equals()와 hashCode() 메서드를 생성한다. 객체의 내용을 비교하고 해싱할 수 있으므로 동등성 비교와 해시 테이블에서의 사용이 용이해진다.

나는 의문이 들었다. 기본 Object 객체에도 equals()hashCode()가 존재하는데 lombok@EqualsAndHashCode는 뭐가 다르다고 하는걸까?

		User user1 = new User();
		user1.setId("userId");

		User user2 = new User();
		user2.setId("userId");

		System.out.println(user1.hashCode());
		System.out.println(user2.hashCode());
		System.out.println(user1.equals(user2));

@EqualsAndHashCode 어노테이션을 사용하지 않았다면 (Object 객체의 hashCode(), equals() 함수 사용했다면), 두 객체의 hashCode가 다르게 나오고 equals() 함수는 false를 리턴한다.

→ Java의 Object 객체의 함수는 객체의 참조 (메모리 주소) 기반으로 동작하기 때문이다

 

하지만 어노테이션을 사용했다면, 두 객체의 hashCode 값은 동일하고, equals() 함수는 true를 리턴한다.

Lombok 어노테이션은 메모리 주소를 참조하는 것이 아니라, 객체의 내용을 비교하기 때문이다

 

@EqualsAndHashCode(exclude = {"name", "age"})

위와 같이 선언하면 name, age 컬럼 내용은 제외하고 id 컬럼의 값만 비교하겠다는 뜻이 된다.

@EqualsAndHashCode(of = {"id"})

id 값만 비교하겠다는 뜻이다. 위와 동일하다.  이 외에도 callSuper, onParam 등 여러가지 속성이 있다고 한다.

 

 

어노테이션 (그리고 속성) 선언만으로도, hashCode() equals() 함수를 오버라이드하여 작성하는 수고를 줄여준다.


@AllArgsConstructor / @NoArgsConstructor

 

생성자를 자동으로 생성하는 어노테이션이다.

@AllArgsConstructor는 모든 컬럼(필드)를 인자로 받는 생성자를 생성한다.

 

@NoArgsConstructor는 파라미터 없는 기본 생성자를 생성한다

@NoArgsConstructor(force=true)

옵션을 사용하면, 사용자 정의 생성자가 이미 존재하는 경우에도 기본 생성자를 강제로 생성한다.

또한 컬럼(필드) 값들을 null, 0, false 등과 같은 값으로 초기화 시켜준다


@Builder

 

		User user = User.builder()
					.id("userId")
					.name("userName")
					.age(10)
					.build();

선언만으로도, 빌더 패턴을 생성하여 객체를 쉽게 생성할 수 있도록 해준다


@Data

 

@Getter, @Setter, @EqualsAndHashCode, @ToString 의 어노테이션을 한 번에 적용할 수 있다

 


@Slf4j

 

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {

	@Autowired
	private UserService userService;

	@GetMapping("all")
	public List<String> getAllUser() throws Exception {

		User user = User.builder()
					.id("userId")
					.name("userName")
					.age(10)
					.build();
		
		log.info(user.toString());
		
		
		return userService.getAllUser();
	}

}

SLF4J(Logger)를 사용할 수 있다

 


Lombok 설치

 

Download

 

projectlombok.org

Lombok 파일을 다운로드 받고, 파일이 있는 위치로 가서 java -jar 명령어를 실행
Install / Update. Eclipse가 실행되어있다면 재시작

https://mvnrepository.com/artifact/org.projectlombok/lombok

적당한 버전의 Lombok을 선택합니다. 제 프로젝트는 gradle로 생성했습니다

compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.10' 의 내용을 프로젝트 build.gradle의 dependencies 안에 추가해줍니다.

Project 우 클릭 - Gradle - Refresh Gradle Project
lombok 라이브러리가 추가된 것을 확인

 

'JAVA > Spring' 카테고리의 다른 글

[Springboot] MyBatis (feat. MySQL)  (1) 2023.09.11
[Springboot] MVC 패턴과 폴더 구조  (0) 2023.09.04
[Springboot] 스프링부트 시작하기  (0) 2023.09.03
 

Eclipse Downloads | The Eclipse Foundation

The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks.

www.eclipse.org

Help - Eclipse Marketplace
Spring Tools 4 - Install
Confirm
I accept the terms of the license agreements - Finish
체크 박스 클릭 - Trust Selected
체크 박스 클릭 - Trust Selected
Restart Now
Spring Starter Project
Next
필요한 것들은 나중에 추가해보자. Spring Boot DevTools, Spring Web 체크 후, Finish

 

Project 우 클릭 - Run As - Spring Boot App 클릭하여 서버 실행
8080 포트로 서버 실행


Springboot 개발 환경 구축이 끝났다. 개발을 시작해보자

  1. Lombok
  2. MVC Pattern
  3. MyBatis
  4. yaml
  5. RestAPI
  6. Logging
  7. Batch
  8. + + + + +

'JAVA > Spring' 카테고리의 다른 글

[Springboot] MyBatis (feat. MySQL)  (1) 2023.09.11
[Springboot] MVC 패턴과 폴더 구조  (0) 2023.09.04
[Springboot] Lombok  (0) 2023.09.04

docker-compose를 통해  redis 연동 node express 앱을 만들어보는 블로그를 봤다.

 

node와 redis 컨테이너로 별도로 실행시켜주면, 다른 컨테이너 환경이기 때문에 통신을 할 수 없다.

 

그래서 Docker-Compose를 통해 같은 컨테이너 환경에서 실행할 수 있다.

 

그러나 아래와 같은 오류가 계속 발생했다

  1. return Promise.reject(new errors_1.ClientClosedError());
  2. The client is closed

redis 패키지가 버전 3에서 4 (연결 비동기 처리 방식)로 넘어가면서, 문제가 발생했다고 한다.

나는 redis npm 모듈을 그냥 설치했더니, 최신 버전 (v4.6.8)로 설치됐다..

 

$ npm i redis@3.0.0

 

 

특정 버전 선언을 통해 설치하면 될 것 같다. (해결하고 나서 생각났다...)

 


프로젝트 생성

$ npm init -y
$ npm i express
$ npm i redis
$ npm i body-parser

 

package.json

{
  "name": "docker-k8s",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "redis": "^4.6.8"
  },
  "type": "module"
}

type=module 옵션을 통해, 모듈을 require가 아닌 import 키워드를 통해 가져오도록 설정

 

index.js

import express from "express";
import { set, get } from "./redis.js";
import bodyParser from "body-parser";

const app = express();
const PORT = 8080;

app.use(bodyParser.urlencoded({extended:true}));
app.use(bodyParser.json());

app.get("/", async (req, res) => {
  res.send("running app !");
});

app.get("/:key", async (req, res) => {
  const key = req.params.key;
  const value = await get(key);

  res.status(200).send(`redis get ${key} : ${value}`);
});

app.post("/", async (req, res) => {
  const key = req.body.key;
  const value = req.body.value;

  await set(key, value);

  res.status(201).send(`redis set ${key} ${value}`);
});

app.listen(PORT, () => {
  console.log(`Running Server: ${PORT}`);
});

 

redis.js

import redis from "redis";

const client = redis.createClient({
  url: `redis://redis-hostname`,
});

const run = async () => await client.connect();

run();

const set = async (key, value) => await client.set(key, value);
const get = async (key) => await client.get(key);

export { set, get };

Docker-compose.yml 에서 설정한 redis의 hostname을 url에 넣어줍니다

 

Dockerfile

FROM node
# 프로젝트 경로
WORKDIR /Users/sejin/Documents/docker-k8s
COPY package.json ./
RUN npm install
COPY ./ ./
CMD [ "node", "index.js" ]

 

Docker-compose.yml

version: '1' # 버전 관리를 위함. 큰 의미는 없다
services:
  node-app: # 원하는 서비스 이름 선언
    build: . # Dockerfile이 동일한 경로에 존재하여 현재 경로의 Dockerfile을 빌드
    ports:
      - 8080:8080 # 왼쪽 포트는 외부, 오른쪽 포트는 내부 포트를 지정한다. 포트포워딩 용도
    depends_on: # 종속성을 정의. 다른 서비스의 시작을 기다림
      - redis-service # redis 서비스명을 입력하였고. redis 시작 후 node 앱이 실행된다
  redis-service:
    image: redis
    command: redis-server --port 6379
    container_name: redis-container
    hostname: redis-hostname
    labels:
      - "name=redis"
      - "mode=standalone"
    ports:
      - 6379:6379

 


Docker compose 실행하기

# 작업 공간 (workspace)에서 실행. 이미지가 있다면 바로 실행. 없다면 빌드 후 실행
$ docker-compose up

# 만약 소스에 변경사항이 있다면 빌드 후 실행
$ docker-compose up --build

# detached 모드. 백그라운드에서 실행
$ docker-compose up -d

# 특정 경로에 있는 docker-compose 파일 실행
$ docker-compose -f /workspace/docker-k8s/Docker-compose.yml up

 


docker / docker-compose를 실행하면 Docker Desktop에서 실행된 것을 확인할 수 있습니다
docker ps , docker-compose ls 명령어를 통해 컨테이너가 실행되고 있는 것을 확인할 수 있습니다
redis 명령어를 통한 키 값 조회, 설정

Docker Container 접속

$ docker exec -it <container-name> bash

Node Express 서버 (with Redis) API 확인

key: user 의 value 값 아직 없음
key, value 등록
등록한 key: user의 value 값을 들고 오는 것을 확인

 


참고 링크

 

redis + node 연동

node 에서 redis 를 이용하기 위해 구글링으로 example code를 찾아보았다. 그러나 돌아온 결과는 보기 좋게 Error를 띄웠다. 하지만 stackoverflow는 모르는게 없는법. 바로 관련된 자료를 찾으러 달려갔다.

velog.io

 

 

Redis NodeJs server error,client is closed

I am developing an application where chats has to cached and monitored, currently it is an local application where i have installed redis and redis-cli. The problem i'm facing is (node:5368)

stackoverflow.com

 

'Docker & k8s' 카테고리의 다른 글

Docker 시작하기  (0) 2022.04.02

인텔 맥북 환경에서 설치 진행

 

 

Download Ubuntu Desktop | Download | Ubuntu

Ubuntu is an open source software operating system that runs from the desktop, to the cloud, to all your internet connected things.

ubuntu.com

VirtualBox에서 사용할 Ubuntu iso 이미지 파일 다운로드

 

 

Downloads – Oracle VM VirtualBox

Download VirtualBox Here you will find links to VirtualBox binaries and its source code. VirtualBox binaries By downloading, you agree to the terms and conditions of the respective license. If you're looking for the latest VirtualBox 6.1 packages, see Virt

www.virtualbox.org

VirtualBox 프로그램 다운로드

 

 

추가(A) 버튼
이름 설정 - 다운받은 ISO 이미지 파일 선택
계정 정보 설정
메모리, CPU Processors 설정
하드 디스크 용량 설정
설정 요약 확인 후, Finish
Try or Install Ubuntu 선택
자동 로그인으로 설정
설치 기다리는 중
설치 완료

  • 만약에 please remove the installation medium. then press enter 문구가 나온다면 엔터
  • 마우스가 맥북으로 빠져 나오지 못한다면, 컨트롤 키를 누르면 빠져나올 수 있었다

우분투 한글 환경 설정

Settings - Region & Language - Manage Installed Languages - Install / Remove Languages... - Korean - Apply - Select Language (Korean) 선택 후, Restart


한글 입력 설정

  • 설정 - 키보드 - 입력소스에서 한영 전환키를 설정할 수 없었고,  터미널  ibus-setup을 통해 한영 전환키 설정을 했다. 하지만 이 설정을 통해 한영전환이 잘되지 않았다
  • 터미널에서 fcitx를 설치한다. ( apt-get install fcitx ). 권한 문제가 있다면 su 명령어를 통해 관리자 권한으로 명령어를 실행

설치 후,&nbsp; 기본 설정이란 항목이 추가된 것을 확인할 수 있고, 한영전환키를 설정할 수 있다


sudoers 설정 파일에 없습니다. 이 시도를 보고합니다. 해결하기

  • su 명령어를 통해 관리자 쉘 권한을 획득 후, vi 또는 gedit 명령어를 이용해 sudoers 파일을 수정 ( gedit  /etc/sudoers )
  • 사용자명  ALL=(ALL)  ALL 을 입력 공백은 탭으로 구분

# IP 주소 확인을 위한 net-tools 설치.  ifconfig 명령어 통한 IP 확인
$ sudo apt install net-tools
# Node.js NPM 설치
$ sudo apt install nodejs npm

# Node 최신 버전으로 업데이트
$ npm install -g n
$ n latest
$ n prune
# Chrome 설치
$ wget wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
$ sudo apt install ./google-chrome-stable_current_amd64.deb

'Linux' 카테고리의 다른 글

bashrc 적용 오류  (0) 2023.09.28
// package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "NODE_ENV=DEV node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
// index.js
console.log(process.env.NODE_ENV);
npm run start

 

MacOS 환경에서 start 스크립트를 실행하면 파일에서 선언한 NODE_ENV 값(DEV)을 사용할 수 있다

하지만 윈도우OS 환경에서 start 스크립트를 실행하면 값이 제대로 들고 오지 못할 때가 있다

 

npm run start
// package.json
{
  "scripts": {
    "start": "cross-env NODE_ENV=DEV node index.js"
}

 

cross-env 패키지 설치 후, 실행하면 윈도우 환경에서도 환경 변수 값을 사용할 수 있다

// package.json
{
  "scripts": {
    "start:windows": "cross-env NODE_ENV=DEV node index.js",
    "start:mac": "NODE_ENV=DEV node index.js"
}

'JavaScript > Node' 카테고리의 다른 글

PM2를 활용한 앱 관리  (0) 2023.08.05
[Node Express] PayloadTooLargeError  (0) 2023.05.11
카페24 노드 서버 구축하기  (2) 2020.12.01

Guard

Guard Process

 

Guard는 '경비' 라는 뜻대로, Application을 보호하고, 유효한 사용자(authentication, 인증: 애플리케이션의 유효한 사용자인지 확인), 권한(authorization, 인가: 사용자에게 허가된 작업인지 확인)을 검증하는 역할을 한다. 검증된 사용자가 아니면 요청 자체를 막아버리는 것이다. 예를 들면, 사용자에 대한 검증이 없다면 모든 요청에 대해 응답을 할 것이고, 서버 자원에 낭비가 지속되면 서버 부하 / 다운의 결과를 초래할 가능성이 높다.

 

일반적으로 기존 Express.js 서버에서는 미들웨어(middleware)에 의해 처리되었다. 미들웨어는 인증을 위한 휼륭한 선택이지만, 토큰 검증이나 속성 연결과 같은 작업을 관리하기 힘들다.

→ 가드는 모든 미들웨어 이후에 실행되지만 인터셉터나 파이트 이전에 실행된다

 

JWT(Json Web Token)을 활용하여, 사용자 인증과 인가를 구현하는 방법을 작성하였다.

 

  1. 사용자 로그인 성공 시, AccessToken과 RefreshToken을 발급
  2. 로그인 이후 사용자는 Request Header에 AccessToken을 포함하여 요청
  3. 사용자는 AccessToken이 만료되면, 발급 받았던 RefreshToken을 통해 새로운 AccessToken을 발급

추가적으로 사용자 인증 전에는 AccessToken을 가지고 있지 않기 때문에, 로그인 로직에서는 Guard 검증을 거치지 않도록 설정

 


NPM 패키지 설치

npm i --save @nestjs/jwt

📔 src/auth/guard/public.decorator.ts

import { SetMetadata } from "@nestjs/common";

export const Public = () => SetMetadata('isPublic', true)

사용자 로그인 이전에는 Jwt 토큰이 없기 때문에, 함수 데코레이터 선언을 통해 로그인 API에서는 인증 가드 검증을 하지 않도록 할 예정

 

Auth Module, Service, Controller 작성

 

📔 src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '@src/user/user.module';

@Module({
  imports: [JwtModule.register({
    global: true
  }), UserModule],
  providers: [AuthService],
  controllers: [AuthController]
})
export class AuthModule { }

 

📔 src/auth/user.module.ts

더보기
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule { }

UserService를 exports 설정

 

 

📔 src/auth/auth.service.ts

더보기
import { Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '@src/user/user.service';

@Injectable()
export class AuthService {

    private readonly ACCESS_TOKEN_SECRET_KEY: string;
    private readonly REFRESH_TOKEN_SECRET_KEY: string;

    constructor(private readonly jwtService: JwtService
        , private readonly userService: UserService
        , private readonly configService: ConfigService
    ) {
        this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
        this.REFRESH_TOKEN_SECRET_KEY = configService.get<string>('SERVER.REFRESH_TOKEN_SECRET_KEY')
    }

    async signIn(id: string, password: string) {
        // ID를 가진 사용자가 있는지 데이터베이스 조회
        const user = await this.userService.signIn(id);

        if (!user) {
            throw new UnauthorizedException('유저 정보 없음');
        } else if (user?.password !== password) {
            throw new UnauthorizedException('비밀번호 일치하지 않음');
        }

        const payload = { id: user.id, name: user.name }

        return {
            accessToken: await this.jwtService.signAsync(payload, {
                secret: this.ACCESS_TOKEN_SECRET_KEY
                , expiresIn: '10m'
            }),
            refreshToken: await this.jwtService.signAsync(payload, {
                secret: this.REFRESH_TOKEN_SECRET_KEY
                , expiresIn: '2w'
            })
        }
    }

    async getNewAccessToken(refreshToken: string) {
        try {
            // RefreshToken 검증
            const payload = await this.jwtService.verifyAsync(
                refreshToken,
                {
                    secret: this.REFRESH_TOKEN_SECRET_KEY
                }
            )

            // 새로운 AccessToken 발급
            return {
                accessToken: await this.jwtService.signAsync({
                    id: payload.id,
                    name: payload.name
                }, {
                    secret: this.ACCESS_TOKEN_SECRET_KEY
                    , expiresIn: '10m'
                })
            }
        } catch (e) {
            if (e.name === 'TokenExpiredError') {
                throw new UnauthorizedException('refreshToken 만료');
            } else if (e.name === 'JsonWebTokenError') {
                throw new UnauthorizedException('refreshToken 정보 없음');
            }

            throw new InternalServerErrorException(e);
        }
    }

}
  • ConfigService를 통해 환경 변수 관리 (ACCESS_TOKEN_SECRET_KEY,  REFRESH_TOKEN_SECRET_KEY)
  • 토큰 만료기간은 AccessToken은 짧게, RefreshToken은 길게 설정
  • JWT 토큰의 Payload(내용)는 사용자의 ID, 이름으로 구분

 

 

 

[NestJS] 환경 변수 (config) 사용하기

환경 변수, 파일을 사용하는 이유 - 프로젝트에서 공통적으로 사용하는 변수들을 파일을 통해 쉽게 관리할 수 있다 - 코드에 secret key 값을 노출시키지 않기 때문에 보안 상 이점이 있다 - 운영(pro

clichy12.tistory.com

 

 

📔 src/auth/auth.controller.ts

더보기
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from './guard/public.decorator';

@Controller('auth')
export class AuthController {

    constructor(
        private readonly authService: AuthService
    ) { }

    @HttpCode(HttpStatus.OK)
    @Post('sign-in')
    @Public()
    signIn(@Body() user: Record<string, any>) {
        return this.authService.signIn(user.id, user.password);
    }

    @HttpCode(HttpStatus.OK)
    @Post('refresh')
    @Public()
    getNewAccressToken(@Body() body: Record<string, any>) {
        return this.authService.getNewAccessToken(body.refreshToken);
    }

}

Public 데코레이터를 선언하여,  로그인이나 새로운 AccessToken을 발급 받는 API 요청 시에는 인증 가드 검증을 하지 않도록 설정

 


Auth Guard 작성

 

📔 src/auth/guard/Auth.guard.ts

import { CanActivate, ExecutionContext, Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";


@Injectable()
export class AuthGuard implements CanActivate {

    private readonly ACCESS_TOKEN_SECRET_KEY: string;

    constructor(private jwtService: JwtService
         , private configService: ConfigService
         , private reflector: Reflector
        ) {
            this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
        }

    async canActivate(context: ExecutionContext): Promise<boolean>{
        // Public 데코레이터를 선언한 라우터 경로는 JWT 토큰 검증을 하지 않음 (ex. 로그인)        
        const isPublic = this.reflector.get('isPublic', context.getHandler());
        if(isPublic){
            return true;
        }

        const request = context.switchToHttp().getRequest();

        // Request Header Authorization. JWT AccessToken 추출
        const token = this.extractTokenFromHeader(request);
        
        if(!token) {
            throw new UnauthorizedException('accessToken 필요');
        }

        try {
            // AccessToken 검증
            const payload = await this.jwtService.verifyAsync(
                token,
                {
                    secret: this.ACCESS_TOKEN_SECRET_KEY
                }
            )

            // 라우트 핸들러에서 접근할 수 있도록 Request에 payload를 할당
            request['user'] = payload;
        } catch (e){            
            if (e.name === 'TokenExpiredError') {
                throw new UnauthorizedException('accessToken 만료');
            } else if (e.name === 'JsonWebTokenError') {
                throw new UnauthorizedException('accessToken 정보 없음');
            }
            
            throw new InternalServerErrorException(e);
        }
        
        return true;
    }

    private extractTokenFromHeader(request: Request) : string | undefined {
        const [type, token] = request.headers.authorization?.split(' ') ?? [];
        return type === 'Bearer' ? token : undefined;
    }
}
  • Public 데코레이터를 선언한 라우터 경로는 검증 하지 않음
  • Header Authorization에 있는 JWT AccessToken 검증
  • 비즈니스 로직에서 JWT 정보를 사용할 수 있도록, Request에 Payload를 할당
  • canActivate 함수에서 false를 반환하면  403 Forbidden Error를 반환
  • 토큰 만료 시, 401 Unauthorized Error를 반환하도록 설정
  @Get()
  findAll(@Req() req: Request) {
    const user = req['user'];
    console.log(user);
    
    return this.productService.findAll();
  }

 

 

📔 src/app.module.ts

	...
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { AuthGuard } from './auth/guard/Auth.guard';

@Module({
  imports: [
                ...
                AuthModule
        	],
  controllers: [
  				...
  ],
  providers: [
  				...
  			{
            provide: APP_GUARD,
            useClass: AuthGuard
		 }],
})
export class AppModule implements NestModule {
	...
}

accessToken 없이 요청 시, 401 Error 발생
로그인 성공 시, 클라이언트에게 accessToken, refreshToken을 반환
발급 받은 AccessToken 값을 요청 헤더에 포함하여 요청 &rarr; 인증 가드 검증 성공
accessToken 만료 시, 클라이언트는 발급 받은 refreshToken을 통해 새로운 accessToken을 발급

 


+ Redis (NoSQL), RDBMS 를 통해  accessToken, refreshToken 관리

+ 사용자 로그아웃 시, JWT Token 폐기 로직


참고 링크

 

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 Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)

Cookie / Session / Token 인증 방식 종류 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해

inpa.tistory.com

 

 

[NestJS] Guard 사용하기

웹서버에서 중요한 것 중의 하나는 보안 관련한 로직일 것이다. NestJS에서는 허용된 User가 아니면 query나 Mutation, Subscription을 요청하지 못하도록 하는 Middleware를 제공한다. 이것을 UseGuard라는 decora

tre2man.tistory.com

 

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

[NestJS] Exception filters  (0) 2023.08.21
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

 

Client Side &rarr; Pipe &rarr; Route Handler &rarr; Filter &rarr; Client Side 과정을 거친다

 

NestJS는 예외 레이어(exceptions layer)가 내장되어 있어서, 애플리케이션 전체에서 처리되지 않은 예외를 처리할 수 있다

 

애플리케이션 (Application Code) 예외 처리

  • O : Exception Filter 에서 처리하지 않는다
  • X : Filter 에서 예외를 포착하여 적절한 사용자 친화적인 응답을 자동으로 보낸다

 


Global Exceptions Filter 생성

 

📔 src/middleware/exception/HttpExceptionsFilter.ts

@Catch() 데코레이터의 매개변수 목록을 비워, 처리되지 않은 모든 예외를 포착할 수 있다

import { ArgumentsHost
    , Catch
    , ExceptionFilter
    , HttpException
    , HttpStatus } from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";

@Catch()
export class HttpExceptionsFilter implements ExceptionFilter {
    constructor(private readonly httpAdapterHost: HttpAdapterHost) { }

    catch(exception: unknown, host: ArgumentsHost): void {
        const { httpAdapter } = this.httpAdapterHost;

        const ctx = host.switchToHttp();
        
        const httpStatus =
            exception instanceof HttpException
                ? exception.getStatus()
                : HttpStatus.INTERNAL_SERVER_ERROR; 
        
        const responseBody = {
            statusCode: httpStatus,
            timestamp: new Date().toLocaleString(),
            path: httpAdapter.getRequestUrl(ctx.getRequest()),
            message: "Http Exceptions Filtering",
            message1: "message1"
        }

        httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
    }
}

 

📔 src/main.ts

	...
    
import { HttpExceptionsFilter } from '@src/middleware/exception/HttpExceptionsFilter'


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

  const httpAdapter = app.get(HttpAdapterHost);
  app.useGlobalFilters(new HttpExceptionsFilter(httpAdapter));

	...

  await app.listen(port);
}
bootstrap();

 

Exception Layer Filtering

 


참고 링크

https://docs.nestjs.com/exception-filters

https://tristy.tistory.com/52

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

[NestJS] Guard + JWT 구현하기  (0) 2023.08.27
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

 

Swagger(스웨거)는 개발자가 REST 웹 서비스를 설계, 빌드, 문서화, 소비하는 일을 도와주는 대형 도구 생태계의 지원을 받는 오픈 소스 소프트웨어 프레임워크이다. 대부분의 사용자들은 Swagger UI 도구를 통해 스웨거를 식별하며 툴셋에는 자동화된 문서화, 코드 생성, 테스트 케이스 생성 지원이 포함된다. - wikipedia

 

 

NPM 패키지 설치

$ npm install --save @nestjs/swagger

 

 

src/config/swagger.config.ts

import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"

export default app => {
    const config = new DocumentBuilder()
        .setTitle('nest project')
        .setDescription('description')
        .setVersion('1.0.0')
        .build();

    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('swagger', app, document);
}
  • http://localhost:3000/swaggerSwagger UI 접속

 

 

DocumentBuilder Options

더보기
import { OpenAPIObject } from './interfaces';
import { ExternalDocumentationObject, ParameterObject, SecurityRequirementObject, SecuritySchemeObject, ServerVariableObject } from './interfaces/open-api-spec.interface';
export declare class DocumentBuilder {
    private readonly logger;
    private readonly document;
    setTitle(title: string): this;
    setDescription(description: string): this;
    setVersion(version: string): this;
    setTermsOfService(termsOfService: string): this;
    setContact(name: string, url: string, email: string): this;
    setLicense(name: string, url: string): this;
    addServer(url: string, description?: string, variables?: Record<string, ServerVariableObject>): this;
    setExternalDoc(description: string, url: string): this;
    setBasePath(path: string): this;
    addTag(name: string, description?: string, externalDocs?: ExternalDocumentationObject): this;
    addSecurity(name: string, options: SecuritySchemeObject): this;
    addGlobalParameters(...parameters: ParameterObject[]): this;
    addSecurityRequirements(name: string | SecurityRequirementObject, requirements?: string[]): this;
    addBearerAuth(options?: SecuritySchemeObject, name?: string): this;
    addOAuth2(options?: SecuritySchemeObject, name?: string): this;
    addApiKey(options?: SecuritySchemeObject, name?: string): this;
    addBasicAuth(options?: SecuritySchemeObject, name?: string): this;
    addCookieAuth(cookieName?: string, options?: SecuritySchemeObject, securityName?: string): this;
    build(): Omit<OpenAPIObject, 'paths'>;
}

 

 

SwaggerModule.setup의 첫번째 매개변수는 swagger path를 의미

 

 

src/main.ts

	...
import swagger from './config/swagger.config';

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

	...

  swagger(app);

  await app.listen(port);
}
bootstrap();

 

 

src/app/app.controller.ts  →  @ApiTags. @ApiOperation. @ApiOkResponse. @ApiBadRequestResponse

	...

@ApiTags('app tag')
@Controller()
export class AppController {

  constructor(private readonly appService: AppService) { }

  @ApiOperation({
    summary: '서버 상태 검사',
    description: '정상적으로 서버가 실행되고 있는지 확인'
  })
  @ApiOkResponse({
    description: 'OK'
  })
  @ApiBadRequestResponse({
    description: 'Bad Request'
  })
  @Head()
  getStatus() {
    this.appService.getStatus();
  }
  
  	...

 

 

swagger ui 확인

 

 

 


참고 링크

https://docs.nestjs.com/openapi/introduction

https://any-ting.tistory.com/122

https://www.npmjs.com/package/@nestjs/swagger

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

[NestJS] Guard + JWT 구현하기  (0) 2023.08.27
[NestJS] Exception filters  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

TypeORM 패키지 설치

$ npm install --save @nestjs/typeorm typeorm mysql2

 

MySQL 설치

 

[MacOS] MySQL 설치

brew 설치 $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" Homebrew Site MySQL 설치, 설정 # Installation $ brew install mysql # version $ mysql -V # server start $ mysql.server start # server stop $ my

clichy12.tistory.com

 

User 테이블 생성

CREATE TABLE `User` (
  `idx` bigint unsigned NOT NULL AUTO_INCREMENT,
  `id` varchar(100) NOT NULL,
  `password` varchar(100) NOT NULL,
  `name` varchar(100) NOT NULL,
  `createDt` timestamp NULL DEFAULT NULL,
  `updateDt` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`idx`)
)

 


 

# res: resource
$ nest g res user

spec.ts 파일은 삭제


src/config/typeorm.config.service.ts

더보기
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from "@nestjs/typeorm";
import { ConfigService } from "@nestjs/config";
import { Injectable } from "@nestjs/common";

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {

    constructor(private readonly configService: ConfigService) { }

    createTypeOrmOptions(connectionName?: string): TypeOrmModuleOptions | Promise<TypeOrmModuleOptions> {

        return {
            type: 'mysql',
            host: this.configService.get('DB.MYSQL.HOST'),
            port: this.configService.get('DB.MYSQL.PORT'),
            username: this.configService.get('DB.MYSQL.USER'),
            password: String(this.configService.get('DB.MYSQL.PASSWORD')),
            database: this.configService.get('DB.MYSQL.DATABASE'),
            entities: [],
            synchronize: false,
            autoLoadEntities: true
        }
    }

}

 

entities : TypeORM에 Entity를 명시적으로 알린다, autoLoadEntities: true 를 사용하기 때문에 빈 배열을 넣었다

synchronize : 운영 환경에서 true 값을 사용하게 되면, 데이터가 손상될 수 있다

autoLoadEntities : true 일 경우, Entity가 자동으로 로드된다. forFeature() 선언을 통해 자동으로 추가될 예정

  • user.module.ts에서 TypeOrmModule.forFeature를 통해 선언 할 예정

src/app.module.ts

더보기
...
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './config/typeorm.config.service';

@Module({
  imports: [
  		...
  	, TypeOrmModule.forRootAsync({
      useClass: TypeOrmConfigService
    })],
	...
})
export class AppModule implements NestModule {
	...
}

src/user/entities/user.entity.ts

더보기
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    idx: number;

    @Column()
    id: string;

    @Column()
    password: string;

    @Column()
    name: string;

    @Column()
    createDt: Date;

    @Column()
    updateDt: Date;
}

 

데이터베이스 User 테이블과 동일한 속성으로 선언


src/user/user.module.ts

더보기
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

$ npm i class-validator

 

데코레이터(@) 선언만으로  API 요청 시, 데이터 검증을 자동으로 해준다

@IsOptional()을 선언하지 않으면 해당 속성은 필수 속성이 된다

 


src/user/dto/create-user.dto.ts

더보기
import { IsDate, IsOptional, IsString, Length } from 'class-validator';

export class CreateUserDto {

    @Length(5, 50)
    @IsString()
    readonly id: string;

    @IsString()
    readonly password: string;

    @IsString()
    readonly name: string;

    @IsOptional()
    @IsDate()
    readonly createDt: Date = new Date();

    @IsOptional()
    @IsDate()
    readonly updateDt: Date = new Date();

}

src/user/dto/update-user.dto.ts

더보기
import { IsDate, IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {

    @IsOptional()
    @IsString()
    readonly password: string;

    @IsString()
    readonly name: string;

    @IsOptional()
    @IsDate()
    readonly updateDt: Date = new Date();

}

src/user/user.service.ts

더보기
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class UserService {

  constructor(@InjectRepository(User) private userRepository: Repository<User>) { }

  async create(createUserDto: CreateUserDto) {
    const user = await this.findOne(createUserDto.id);

    if (user.length > 0) {
      return [];
    }

    return await this.userRepository.save(createUserDto);
  }

  async findAll() {
    return await this.userRepository.find();
  }

  async findOne(id: string) {
    return await this.userRepository.find({ select: ['idx', 'id', 'name', 'createDt', 'updateDt'], where: { id } })
  }

  update(id: string, updateUserDto: UpdateUserDto) {
    console.log(id, updateUserDto);

    return this.userRepository.update({ id }, updateUserDto);
  }

  remove(id: string) {
    return this.userRepository.delete({ id });
  }
}

src/user/user.controller.ts

더보기
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.userService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(id, updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.userService.remove(id);
  }
}

$ npm i class-transformer

 

 

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 Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

...
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true
    })
  )
...

ValidationPipe 선언만으로 검증을 해줄 수 있다

 

 


참고 링크

https://docs.nestjs.com/techniques/validation

https://velog.io/@artlogy/08.-Validate

https://docs.nestjs.com/techniques/database

https://codegear.tistory.com/116

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

[NestJS] Exception filters  (0) 2023.08.21
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19
[NestJS] Jest 테스트하기  (0) 2023.08.17

환경 변수, 파일을 사용하는 이유

- 프로젝트에서 공통적으로 사용하는 변수들을 파일을 통해 쉽게 관리할 수 있다

- 코드에 secret key 값을 노출시키지 않기 때문에 보안 상 이점이 있다

- 운영(prod), 개발(dev), 로컬(local) 환경에 맞는 다른 변수를 사용할 수 있다

 


패키지 설치

$ npm i @nestjs/config

 

src/app.module.ts

import { ConfigModule } from '@nestjs/config';
...

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true
    }),
    ....]
...

루트 모듈의 설정을 통해 환경 변수에 접근할 수 있도록 할 수 있다

isGlobal: true 속성을 통해 다른 모듈에서 사용할 수 있도록 설정하였고, cache: true 속성을 통해 한 번 읽은 환경 변수의 값을 캐싱하여 읽기 속도를 향상 시킬 수 있다

 

위 설정만으로 .env 파일에 선언된 속성을 바로 사용할 수 있다

 

프로젝트 루트 경로/.env

NODE_ENV=DEV
SERVER_PORT=3000

 

 

main.ts

	...

const SERVER_PORT = process.env.SERVER_PORT 

async function bootstrap() {
	...
  await app.listen(SERVER_PORT);
} 
bootstrap();

개발 환경에 따른 환경 변수 사용 방법

 

.env

NODE_ENV=DEV

 

src/config/dev.yaml 또는 prod.yaml

SERVER:
  PORT: 3000

DB:
  MYSQL:
    HOST: 127.0.0.1
    USER: root
    PASSWORD: root
    PORT: 3306

 

src/config/config.ts

import { readFileSync } from 'fs'
import * as yaml from 'js-yaml'
import { join } from 'path'

const YAML_CONFIG_PROD = 'prod.yaml'
const YAML_CONFIG_DEV = 'dev.yaml'

export default () => {
    return yaml.load(
        process.env.NODE_ENV === 'DEV' ?
            readFileSync(join(__dirname, YAML_CONFIG_DEV), 'utf8') :
            readFileSync(join(__dirname, YAML_CONFIG_PROD), 'utf8')
    ) as Record<string, any>
}

 

src/app.module.ts

...
import config from './config/config';

@Module({
  imports: [ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
      load: [config]
    })],
  ...
})

 

src/main.ts

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

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

  const configService = app.get(ConfigService);
  const SERVER_PORT = configService.get<string>('SERVER.PORT');
  
  await app.listen(SERVER_PORT);
} 
bootstrap();

 

 

$ npm run start:dev

서버 실행

 

yaml 파일은 compile 시에, dist 폴더로 복사되지 않기 때문에 파일을 찾지 못하는 문제가 발생

 

패키지 설치

$ npm i cpx

 

 

package.json

...
"scripts": {
    "copy-files": "cpx \"src/config/*.yaml\" dist/config/",
    ...
    "start": "npm run copy-files && nest start",
    "start:dev": "npm run copy-files && nest start --watch",
    "start:debug": "npm run copy-files && nest start --debug --watch",
    "start:prod": "npm run copy-files && node dist/main",
    ...
  },
  ...

컴파일 시에 dist/config 경로에 src/config/*.yaml 파일이 위치할 수 있도록 복사해준다

 

 

nest-cli.json  

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": false
  }
}

package.json - scripts 수정 후 실행해도 똑같은 오류가 발생한다. 파일이 복사되지만 바로 파일이 삭제되기 때문이다

deleteOutDir 속성 값을  true false 로 변경해 준다

 


참고 링크

https://codegear.tistory.com/82

https://www.daleseo.com/nestjs-configuration/

https://velog.io/@kimjiwonpg98/NEST-.env-%ED%8C%8C%EC%9D%BC-%EA%B4%80%EB%A6%AC%EB%B2%95-feat.-nestjsconfig

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

[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] Logging 하기  (0) 2023.08.19
[NestJS] Jest 테스트하기  (0) 2023.08.17
[NestJS] 경로 alias 설정하기  (0) 2023.08.17

+ Recent posts