Lsiron

Nest.js에서 Controller와 Service 다루기 본문

백엔드/Nest.js

Nest.js에서 Controller와 Service 다루기

Lsiron 2024. 8. 22. 18:21

이제 기본적인 CRUD API를 만들면서 Nest.js의 핵심 개념인 Controller와 Service의 역할을 알아보자.

 

controller는 URL을 매핑하고, 리퀘스트를 받고 query를 넘기거나, body나 그 외의 것들을 넘기는 역할을 했었다.

 

허나, service는 로직을 관리하는 역할을 맡을 것 이다. 이렇게 한 개의 요소가 한 가지 기능은 꼭 책임져야 한다.

 

이것이 바로 single-respnsibility principle 이다.

( 하나의 module, class 혹은 function이 하나의 기능은 꼭 책임져야 한다는 것 )

 

이 점을 명심하면서, service를 만들어보겠다. 

 

터미널에 아래와 같이 명령어를 입력하여 서비스를 만들어보자

$ nest g s movies

 

 

위와 같이 movies의 service가 만들어진 것을 확인 할 수 있다.

 

이제 app.module.ts를 보면 service 또한 넣어져있는 것을 확인 할 수 있다.

 

 

다음으로 할 것은 service에 데이터베이스를 만들 것 이다. movies.service.ts로 가보자.

 

service를 만들기 위해 간단하게 가짜 데이터베이스를 만들어 보겠다.

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

@Injectable()
export class MoviesService {
    private movies = [];
}

 

빈 배열로 설정한 movies에 private을 지정하고, entities 폴더를 하나 만들어 준 뒤, movie.entity.ts 파일을 만들어주자.

( 보통은 entities에 실제로 데이터베이스의 모델을 만들어 주어야 한다. )

 

이 후, movie.entity.ts 파일에서는 서비스로 보내고 받을 클래스(인터페이스)를 export 해 줄 것이다.

//movie.entity.ts
export class Movie {
    id: number;
    title: string;
    year: number;
    genres: string[];
}

 

위 내용이 movie를 구성하는 내용이다. 다시 movies.service.ts로 가보자.

import { Injectable } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

@Injectable()
export class MoviesService {
    private movies: Movie[] = [];
}

 

위와 같이 movie.entity.ts 로 부터 타입을 가져와서 movies에 배열 타입으로 지정해 주자.

 

그리곤 이 전에 만들었던 movies.controller.ts 기반으로 서비스 로직을 만들어보자.

 

아래는 movies.controller.ts 이다. ( search endpoint를 이용한 API만 빼고 모두 그대로 가져왔다. )

import { Controller, Get, Post, Delete, Patch, Param, Body } from '@nestjs/common';

@Controller('movies')
export class MoviesController {
    @Get()
    getAll(){
        return "This will return all movies";
    }

    @Get('/:id')
    getOne(@Param('id') movieId: string){
        return `This will return one movie with the id ${movieId}`;
    }

    @Post()
    create(@Body() movieData) {
        return movieData;
    }

    @Delete('/:id')
    remove(@Param('id') movieId: string) {
        return `This will delete a movie with the id: ${movieId}`;
    }

    @Patch('/:id')
    patch(@Param('id') movieId: string, @Body() updateData) {
        return {
            updatedMovie: movieId,
            ...updateData,
        };
    }
}

 

아래는 movies.service.ts이다.

import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

@Injectable()
export class MoviesService {
    private movies: Movie[] = [];

    getAll(): Movie[] {
        return this.movies;
    }

    getOne(id:string): Movie {
        return this.movies.find(movie => movie.id === parseInt(id));
    }

    create(movieData){
        this.movies.push({
            id: this.movies.length + 1,
            ...movieData
        })
    }
    
    delete(id: string) {
        this.getOne(id)
        this.movie = this.movies.filter(movie => movie.id !== parseInt(id));
    }
    
    update(id: string, updateData) {
        const movie = this.getOne(id);
        this.delete(id);
        this.movies.push({...movie, ...updateData })
    }

}

 

update의 경우 삭제를 하고나서 다시 데이터를 집어넣는 로직으로 구현했는데 가짜 데이터베이스를 사용하기 때문에 어쩔 수 없다......

 

이제 service 로직을 구현했으니, movies.controller.ts 로 가서, service에 있는 로직을 가져와주자.

@Controller('movies')
export class MoviesController {
    constructor(private readonly moviesService: MoviesService) {}
}

 

수동으로 import 하는 방법이 아닌, constructor로 MovieService를 초기화 시켜 줌으로써 MoviesController 에서 MovieService를 사용할 수 있다.

 

이제 service 로직과 연결시켜 보자.

import { Controller, Get, Post, Delete, Patch, Param, Body } from '@nestjs/common';
import { MoviesService } from './movies.service';
import { Movie } from './entities/movie.entity';

@Controller('movies')
export class MoviesController {
    constructor(private readonly moviesService: MoviesService) {}

    @Get()
    getAll(): Movie[] {
        return this.moviesService.getAll();
    }

    @Get('/:id')
    getOne(@Param('id') movieId: string): Movie {
        return this.moviesService.getOne(movieId);
    }

    @Post()
    create(@Body() movieData) {
        return this.moviesService.create(movieData);
    }

    @Delete('/:id')
    remove(@Param('id') movieId: string) {
        return this.moviesService.delete(movieId);
    }

    @Patch('/:id')
    patch(@Param('id') movieId: string, @Body() updateData) {
        return this.moviesService.update(movieId, updateData)
    }
}

 

이제 Postman으로 가서 API를 테스트 해보자. 먼저 Post 요청을 통해 하나 생성 해 줄 것이다.

 

201 응답과 함께 요청이 성공적으로 받아들여진 것을 알 수 있다. 이제 Get 요청을 통해 조회를 해 보자.

 

 

성공적으로 조회가 되는 것을 확인 할 수 있다.

 

이제 리팩토링을 한번 해 보자. getOne 을 통해 진행을 해 보겠다.

 

만약 누군가가 아래와 같이 터무니 없는 URL을 입력한다면?

 

아직 아무 처리를 해주지 않았으니 자연스레 200 응답을 보내며 요청이 성공 할 것이다. 하지만 이래선 안된다.

 

movies.service.ts 로 가서 저장되지 않은 movie에 대해 예외처리를 해 주자.

    getOne(id:string): Movie {
        const movie = this.movies.find(movie => movie.id === parseInt(id));
        if(!movie) {
            throw new NotFoundException(`Movie with ID ${id} not found`)
        }
        
        return movie;
    }

 

이제 Postman으로 가서 똑같이 터무니 없는 아이디로 요청을 보내보자.

 

신기한 점이 있지 않은가? 나는 404에러를 주지도 않았고 별도의 설정을 해 주지 않았으며 오로지 NotFoundException 에러를 던져 주기만 했는데 에러 처리가 된다.

 

이는 nest.js에서 기본적으로 제공하는 예외처리이다.

 

즉, HttpExceptin 에서 확장된 nest.js의 제공 기능인 것. 아주 편리하다.

 

이제 삭제기능이 잘 되는지 확인 해 보자.

200 응답을 주면서 삭제기능이 잘 작동하는 것을 볼 수 있다. 다시 한번 터무니없는 id값을 URL에 입력해보자.

 

해당 id의 데이터가 없기 때문에 역시나 404에러가 나온다.

 

마지막으로 업데이트 기능이 잘 작동하는지 확인 해 보자.

200 응답을 주며 잘 실행이 잘 되는것을 확인 할 수 있다. 이제 조회를 해 보자.

역시나 업데이트가 잘 반영된 것을 확인 할 수 있다.

 

허나 우리는 지금 유효성 검사를 하지 않고 있다.

 

다음은 DTO를 통해 유효성 검사를 처리하는 방법을 다뤄보자.

 

참조 : 노마드코더