[ Nest ] postgresDB를 이용한 CRUD 앱 만들기 (2)
🌈 프로그래밍/Nest JS

[ Nest ] postgresDB를 이용한 CRUD 앱 만들기 (2)

반응형

 

안녕하세요? 수구리입니다.

이번 포스팅에서는 지난 포스팅에 이어서 CRUD 앱 만들기를 이어가 보도록 하겠습니다.

이전에 지난 포스팅에서 DTO라는 것을 잠깐 알아보았었는데요~

좀 더 자세히 짚고 넘어가 보도록 하겠습니다.

 

[ DTO 란 ? ]

Data Transfer Object라고 하며, 객체와 Eneity를 매핑해 주는 것을 의미합니다.

즉, 계층간 데이터 교환을 위한 객체를 의미하며 DB에서 데이터를 얻어 Controller 또는 Service에게 전달하는 객체를 의미합니다.

예를 들어, 회원 가입을 위해서 사용자가 id, password, email, address, phone 등등 다양한 정보를 POST 요청을 보낸다고 생각해봅시다.

그러면 Controller에서는 대충 아래와 같이 코드가 구성이 될 겁니다.

@Post()
signUp(
    @Body('name') name: string,
    @Body('id') id: string,
    @Body('password') password: string,
    @Body('email') email: string,
): User {
	return this.userService.signUp(name, id, password, email);
}

이처럼 구성이 될 텐데.. 사용자의 정보가 위에처럼 간단하지 않죠? 엄청 많은 정보를 가지고 있다면

일일이 값을 가져와야하는 그런 문제가 발생합니다.

또한, 많은 프로퍼티를 갖고 있고, 만약 한 곳에서 프로퍼티의 이름을 바꿔주어야 한다면?

모든 곳의 프로퍼티를 수정해야하는 대참사가 일어나게 됩니다. 따라서 이를 해결하기 위해서

이 DTO를 통해서 데이터 유효성을 체크함과 동시에 더 안정적인 코드를 만들어 낼 수 있습니다.

추가로 DTO는 TypeScript의 Type으로도 사용이 가능합니다.

 

[ DTO 생성 ]

아래의 이미지와 같이 폴더와 파일을 만들어줍니다. board의 DTO를 정의하기 위해서입니다.

 

참고로 전체 폴더 트리는 아래의 이미지와 같습니다.

tree ./src /f

 

 

[ create-board.dto.ts ]

import { IsNotEmpty } from "class-validator";

export class CreateBoardDto {
    @IsNotEmpty()
    title: string;

    @IsNotEmpty()
    description: string;
}

DTO는 interface 또는 class로 선언이 가능합니다. 하지만 대게 class가 더 좋다고 하네요..

왜냐하면? class는 JavaScript ES6 표준의 일부이므로 컴파일된 JavaScript에서 실제 Entity로 유지가 됩니다.

하지만, TypeScript Interface는 트랜스파일(!= 컴파일) 도중에 제거가 되므로 Nest가 런타임에서 참조할 수 없습니다!

TypeScript interfaces are removed during the transpilation, Nest can't refer to them at runtime.

따라서, 클래스로 DTO를 만들면 파이프와 같은 기능을 런타임에서 사용할 수 있기 때문에, class로 만들었습니다.

참고로 class-validator라는 모듈을 설치해주어야 @IsNotEmpty()와 같은 데코를 사용할 수 있습니다.

npm install class-validator

 

[ DTO 적용 ]

그렇다면 이제 만든 DTO를 적용하도록 하겠습니다.

대표로 게시물을 만드는 POST 요청에 대한 DTO를 적용한 모습입니다.

[ boards.controller.ts ]

@Post()
createBoard(
    @Body() createBoardDto: CreateBoardDto,
): Promise<Board> {
    return this.boardsService.createBoard(createBoardDto);
}

위와 같이 controller의 코드가 굉장히 간단해집니다.

서비스에서는 controller에서 넘긴 DTO를 받아주어야 하겠죠?

[ boards.service.ts ]

async createBoard(
    createBoardDto: CreateBoardDto,
): Promise<Board> {
    return this.boardRepository.createBoard(createBoardDto);
}

위와 같이 DTO를 controller와 service에 적용하였습니다.

 

[ ID로 특정 board 가져오기 ]

다음으로는 CRUD의 Read 부분에 대해서 살펴보겠습니다.

[ boards.controller.ts ]

@Get('/:id')
getBoardById(
    @Param('id') id: number,
): Promise<Board> {
    return this.boardsService.getBoardById(id);
}

메소드는 GET이며, 인자로 id 값을 가져옵니다.

컨트롤러에서는 서비스로 id값을 넘겨주면서 getBoardById라는 서비스를 호출하게 됩니다.

 

[ boards.service.ts ]

async getBoardById(
    id: number,
): Promise<Board> {
    const found = await this.boardRepository.findOne(id);

    if (!found) {
        throw new NotFoundException(`Can't find Board with id ${id}`);
    }
    
    return found;
}

여기서는 typeOrm에서 제공하는 findOne 함수를 사용합니다.

함수의 형태가 async, await입니다. 이는 데이터베이스 작업이 다 끝난 후 결괏값을 받아올 수 있도록 하기 위함입니다.

만약 이 구문을 사용하지 않는다면, 데이터베이스 작업 도중에 found 값이 결정되어버리기 때문입니다.

 

[ board status 업데이트하기 ]

다음으로는 CRUD의 Update 부분입니다.

[ boards.controller.ts ]

@Patch('/:id/status')
updateBoardStatus(
    @Param('id') id: number,
    @Body('status', BoardStatusValidationPipe) status: BoardStatus,
) {
    return this.boardsService.updateBoardStatus(id, status);
}

Update는 PATCH 요청을 사용합니다.

 

[ boards.service.ts ]

async updateBoardStatus(
    id: number,
    status: BoardStatus,
): Promise<Board> {
    const board = await this.getBoardById(id);

    board.status = status;
    await this.boardRepository.save(board);

    return board;
}

 

게시물의 상태를 업데이트 하기 위해서는 인자로 id와 바꾸려고 하는 status를 받아옵니다.

이때 status는 PUBLIC와 PRIVATE 두 가지입니다. 이 두 값이 아니면 변경할 수 없다는 오류 메시지를 출력해주어야 하므로 여기서 게시물에 대한 Status Pipe를 통해서 값을 검증합니다.

아래는 board의 status를 검증하는 pipe입니다.

 

[ board-status-validation.pipe.ts ]

여기서 Pipe라는 개념이 등장하는데요~

파이프는 말 그대로 관입니다! 

Pipe를 통해서 Controller로 보내는 값이 유효한지, 유효하지 않은지 Check가 가능합니다.

사용자의 입력이 엉뚱한데 모두다 Controller까지 보낼 필요는 없잖아요??

그러니까 Pipe라는 기능을 사용해서 적절하지 않은 값이 온다면 걸러내줍니다.

import { BadRequestException, PipeTransform } from "@nestjs/common";
import { BoardStatus } from "../board-status.enum";

export class BoardStatusValidationPipe implements PipeTransform {

    readonly StatusOptions = [
        BoardStatus.PUBLIC,
        BoardStatus.PRIVATE
    ]

    transform(value: any) {
        value = value.toUpperCase();

        if (!this.isStatusValid(value)) {
            throw new BadRequestException(`${value} isn't in the status`);
        }

        return value;
    }

    private isStatusValid(status: any) {
        const index = this.StatusOptions.indexOf(status);
        return index !== -1;
    }
}

BoardStatusValidationPipe의 전체 코드입니다.

아래쪽에 private isStatusValid라는 함수에서 인자로 받아온 status가 유효한지 아닌지

indexOf 메소드

JS 배열의 indexOf 함수를 사용해서 index를 리턴 받습니다.

값이 유효하다면 index가 0 또는 1이겠죠? 찾지 못한다면 -1을 리턴하네요.

게시물의 상태 업데이트가 잘 되는지 결과를 한번 보겠습니다!

 

[ board 상태 update 테스트 ]

서버를 실행시키고, 요청은 PATCH로 설정하고 아래와 같이 요청을 보내봅니다.

3번 게시물의 상태를 PRIVATE으로 바꾸는 요청입니다.

모든 게시물의 상태를 전, 후로 나누어서 확인해보면 아래와 같습니다.

상태 update 전

 

상태 update 후

정상적으로 잘 바뀌네요! (무야호)

 

[ board 삭제하기 ]

이제는 마지막으로 CRUD의 Delete 부분입니다!

위에서 id를 가지고, 게시물을 조회한 것과 거의 유사합니다. 

[ boards.controller.ts ]

@Delete('/:id')
deleteBoard(
    @Param('id', ParseIntPipe) id,
): Promise<void> {
    return this.boardsService.deleteBoard(id);
}

 

[ boards.service.ts ]

async deleteBoard(
    id: number,
): Promise<void> {
    const result = await this.boardRepository.delete(id);
    if (result.affected === 0) {
        throw new NotFoundException(`Can't find Board with id ${id}`)
    }
}

여기서 delete 함수를 사용했지만 remove 함수도 존재합니다. 

remove 함수는 무조건 존재하는 아이템에 대한 삭제를 진행해야 합니다. 안 그러면 404 error를 발생합니다.

delete 함수는 만약 아이템이 존재하면 지우고 그렇지 않다면 아무런 영향이 없습니다.

따라서 remove 함수는 DB에 2번 접근 (아이템 유무 + 삭제) 하므로 remove가 아니라 delete를 사용합니다.

result의 영향이 없다면 지우지 못한 것이므로 NotFoundException을 던져줍니다.

 

이상으로 DB와 함께 CRUD에 대한 내용과 Pipe 그리고 DTO에 대해서 알아보았습니다.

생각해보니 postgresDB와 연결하고, Nest에서 DB config에 대한 내용이 없네요.. 추가하도록 하겠습니다.

감사합니다.

반응형