Nestjs를 배워보자 9일차 - Repository를 이용한 Board CRUD

5

Nestjs

목록 보기
9/9

Nest

본 강의는 'john ahn'님의 강의를 정리한 내용입니다.
https://www.youtube.com/watch?v=3JminDpCJNE

1. 데이터베이스 연동을 위해 정리

이전에 만들었던 소스코드 부분들을 정리하고, 이제 실제 DB를 붙여보도록 하자

board.model.ts의 board interface는 더 이상 필요하지 않으므로, status만 남기고 삭제해준다.

이제 BoardStatus 만 남으므로 파일명도 board.model.ts에서 board-status.enum.ts로 바꾸어 주도록 하자

controller를 비롯해서 여러군데에서 에러가 발생할텐데 board를 사용하는 곳은 다 주석처리하고, status를 import하는 부분은 import 경로를 바뀐 위치로 바뀌어주자

이제는 local memory에서 실제 db를 사용하는 것으로 코드를 변경할 것이다.

2. DB를 이용하여 실제 데이터를 가져오기 ( Read )

이전 그림을 가져오면

우리는 service에서 repository에 접근하여 DB에 관한 연산을 진행할 것이다. 이는 Repository 패턴으로 TypeORM을 이용한 하나의 패턴이다.

service에서 repository를 사용하기 위해서는 service가 repository를 가지고 있어야 한다. 그런데, 그냥 아무 repository가 아니라, board에 관한 DB연산을 하는 인스턴스를 받아야 하는 것이다.

이를 매번 생성하는 것이 아닌, nestjs에 등록한 repository를 가져와야 하는 것이다. 우리는 이미 이전에 repository를 nestjs에 등록했었다. 바로 board.module.ts의 forFeature를 이용해서 말이다.

아마 2018년까지는 해당 custom repository를 어떻게 만들어야할 지에 대해서 많은 논의가 있었던 것 같다.
https://github.com/nestjs/typeorm/issues/39

참고하길 바란다.

그래서, service를 controller에서 생성자의 인자를 통해 inject한 것처럼, repository도 service의 생성자의 인자를 통해 inject할 수 있다.

뭔가 어려워 보일 수도 있는데, 이는 싱글톤 패턴으로 nestjs가 자동으로 Dependency를 주입시켜주는 것일 뿐이다. 단, controller는 등록된 서비스의 타입에 따라 자동으로 controller에 주입해주었지만, repository는 그렇게 해주진 않는다. 따라서, 별도의 주입 데코레이터가 필요하다.

@Injectable()
export class BoardsService {
    constructor(
        @InjectRepository(BoardRepository)
        private boardRepository : BoardRepository
    ){}
}

바로 다음과 같이 설정해주면 되는 것이다. 생성자의 인자인 boardRepository 인자에 @InjectRepository()로 repository를 선택해주면 자동으로 forFeature에 등록한 repository 중 하나를 넣어주는 것이다. 만약 forFeature에 등록하지 않으면 inject를 해주지 않는다.

이제는 원하는 기능인 getBoardById() 메서드를 생성해보자

단, 두 가지 알고있어야 하는 것이 있는데

  1. TypeORM에서 제공하는 findOne() 메서드 사용
    findOne()은 인자로 들어가는 값에서 가장 먼저 나타나는 튜플을 반환해준다. 단 이는 비동기적이다.
  2. async await을 이용해서 데이터베이스 작업이 끝난 후 결과값을 받을 수 있게 해주기
    db에서는 처리되는 시간이 있기 때문에 처리되는 시간동안 다른 일을 하다가, 처리가 완료된 다음 문제를 해결한다.

service에 다음의 함수를 추가해주면 된다.

async getBoardById(id : number) : Promise<Board> {
    const found = await this.boardRepository.findOne(id);
    
    //found === null
    if(!found){
        throw new NotFoundException(`Can't find Board with id ${id}`)
    }
    return found; 
}

이제 해당 함수를 호출하는 controller부분을 처리해주도록 하자

@Get('/:id')
getBoardById(@Param('id') id : string) : Promise<Board> {
    return this.boardService.getBoardById(Number(id))
}

컨트롤러에 다음의 메서드를 추가해주면 된다.

3. 게시물 생성하기 ( Create )

이번에는 TypeORM의 create() 메서드를 이용하여 튜플을 생성해보도록 하자

service.ts

async createBoard(createBoardDto : CreateBoardDto) : Promise<Board> {
    const {title, description} = createBoardDto;
    const board = this.boardRepository.create({
        title,
        description,
        status : BoardStatus.PUBLIC
    })

    await this.boardRepository.save(board); // db에 만들어진 객체를 저장
    return board
}

참고로 repository.create() 메서드는 단순히 해당 repository entity의 객체를 생성하는 메서드일 뿐이다. 즉, 데이터베이스에 저장하는 것도 아니고, 저장할 데이터를 생성하는 것이기 때문에 async await으로 처리할 필요없다.

그러므로, 만들어진 데이터를 저장하기 위해서 this.boardRepository.save(board)을 사용하는 것이다.

여기는 db에 접속하는 것이므로 async await문법으로 비동기를 제어해주어야 한다.

이제 controller에서 service의 createBoard를 호출하는 메서드를 만들어보도록 하자

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

controller 부분은 크게 어려울 것이 없다. 이전에 사용했던 pipe도 같이 사용하고, Dto로 데이터를 받아와 service에 넘겨주면 끝이다.

이제 제대로 구현했는지 실험해보도록 하자

send 버튼을 눌러주면

다음과 같은 결과가 나온다. id는 자동적으로 typeORM에서 분배해준다. 그럼 해당 게시물이 DB에 잘 저장되어있는지 확인해보도록 하자

pgAdmin으로 들어가보자

먼저 왼쪽 메뉴에서 server/BoardProject/Database/board-app/Schemas/Tables/board 를 클릭한 다음, 위에 table 같이 생긴 버튼을 누르면 오른쪽의 화면이 나온다. 오른쪽 화면의 하단에 있는 테이블이 바로 현재 DB에 있는 데이터이다.

아주 잘 생성되었음을 확인할 수 있다.

이번에는 가져오는 연산을 해보도록 하자

제대로 가져온 것을 확인할 수 있다.

이렇게도 구현할 수 있지만, 사실 이는 좋은 구현이 아니다. 왜냐하면 DB관련된 로직은 repository에서 처리하는것이 의미상 맞기 때문이다. 때문에 service와 repository의 역할을 분리할 필요성이 있다.

그래서 service에서 해준 부분을 repository로 가져가도록 하자

repository

@EntityRepository(Board)
export class BoardRepository extends Repository<Board> {
    async createBoard(createBoardDto : CreateBoardDto) : Promise<Board> {
        const {title, description} = createBoardDto;
        const board = this.create({
            title,
            description,
            status : BoardStatus.PUBLIC
        })

        await this.save(board); // db에 만들어진 객체를 저장
        return board
    }
}

다음과 같이 createBoard를 repository로 가져오도록 하고, 이를 service에서 가져와주면 된다.

service의 createBoard() 메서드는 다음과 같이 변경해주면 된다.

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

4. 게시물 삭제하기 (Delete)

remove() vs delete() ?

remove() : 무조건 존재하는 아이템을 remove 메서드를 이용해서 지워야한다. 그렇지 않으면 에러가 발생한다. (404Error)

delete() : 만약 아이템이 존재하면 지우고, 존재하지 않으면 아무런 영향이 없다.

이러한 차이 때문에 remove를 이용하면 하나의 아이템을 지울 때 아이템이 있는지 check하고, remove해야하므로 총 두 번의 DB연산이 필요하다. 따라서, DB를 한 번만 접근하는 delete 메서드가 더 좋다.

먼저 service부분을 코딩해주도록 하자

async deleteBoard(id : number) : Promise<void> {
    const result = await this.boardRepository.delete(id);
    console.log(result)
}

다음과 같이 service에 deleteBoard() 메서드를 만들어주면 된다.

이제 controller에서 service를 호출해주도록 하자

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

다음과 같이 구현하면 되는데, ParseIntPipe은 내장형 pipe로 해당 파라미터가 반드시 number type으로 변경이 되어야 한다는 것을 의미한다. 만약, number로 변경이 안되는 값이 들어오면 error처리가 나오도록 하는 것이다.

이제 실험해보도록 하자

delete http 메서드로 해당 url을 넣어주면, console에 다음의 로그가 찍힐 것이다.

DeleteResult { raw: [], affected: 1 }

삭제의 결과로 나온 result인데, affected가 1인 것은 1개가 영향을 받았다는 것이다.

한번 더 delete연산을 해주면

DeleteResult { raw: [], affected: 0 }

다음과 같이 영향 받은 것이 없기 때문에 0이 나온다.

우리는 이를 통해 에러처리가 가능하다. 만약 affected가 0이 나오면 id가 없는 것이므로 에러를 던저주는 것이다.

service의 deleteBoard() 메서드 부분을 다음과 같이 고쳐주도록 하자

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}`)
    }
    console.log(result)
}

즉, 없는 id에 접근하여 삭제 연산을 수행하였기 때문에 문제가 발생했다느 것을 반환하는 것이다.

5. 게시물 상태를 변경하기 (Update)

먼저 DB에 게시물이 있는 지 보도록하고, 있다면 정보를 바꾸어준다음에 DB에 바뀐 게시물 객체 정보를 게시해주면된다.

service로 가서 코드를 넣어주도록 하자

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

getBoardById()를 통해서 id에 해당하는 튜플을 가져온 다음에 promise를 풀기위해 await을 사용한다. 이후 board에 status를 변경한 후, 해당 객체를 repository에 save() 해주기만 하면 된다.

이제 controller로 가서 이를 호출하는 부분을 만들도록 하자.

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

patch로 id에 해당하는 튜플의 status를 바꾼다. 지난 번에 만든 pipe를 이용해서 데이터를 검사해주도록 한다.

이제 실험해보도록 하자

현재 DB에 있는 데이터로 id가 2이고 PUBLIC이다. 이를 PRIVATE로 바꾸어주도록 하자

다음과 같이 입력하고 SEND 버튼을 누르면

다음과 같이 DB에 데이터가 변경되고 적용되었음을 확인할 수 있다.

6. 모든 게시물 가져오기

이제 마지막으로 모든 게시물을 가져와보자

repository의 find() 함수를 사용하자, find() 의 인자에 {key : value} 형식으로 해당되는 entity들을 가져온다. 참고로 TypeORM에서 entity란 DB의 table, entity, relation과도 같은 개념이지만, 또 다른 의미로는 DB에 들어가는 객체를 말하기도 한다. 즉, 한 relation안의 튜플들 각각이 TypeORM에서는 객체로 들어가므로 이를 entity라고 설명하는 것이다.

find() 함수에 아무것도 넣어주지 않으면, 이는 빈 조건이므로 모든 entity들을 가져온다. 더 쉽게 말해 해당 relation의 모든 튜플들을 가져온다는 말이다.

service에 다음의 메서드를 넣어주도록 하자

async getAllBoards() : Promise<Board[]> { 
    return this.boardRepository.find()
}

이제는 Board 객체를 여러개 받을 것이므로 배열임을 알려주도록 하자

controller에서 이를 반환해주면 된다.

@Get()
getAllBoards() : Promise<Board[]> {
    return this.boardsService.getAllBoards()
}

이제 postman에서 실험을 해보자

DB에 다음과 같이 데이터들이 있다. 이를 getAllBoards() 메서드를 호출하여 불러와보도록 하자

제대로 결과가 나온 것을 확인할 수 있다.

0개의 댓글