Service & Repository

onyoo·2023년 1월 14일
0

NestJs

목록 보기
3/8

서비스와 리포지토리 ?

이번에는 서비스와 리포지토리가 각각 어떠한 역할을 하고 어떠한 차이점을 가지고 있는지 알아보겠습니다.

어떠한 종류의 비즈니스 논리를 작성할 때 우리는 서비스를 만들어 그곳에 비즈니스 로직을 작성할 것입니다.

실행할 수 있는 무언가나,,일종의 계산같은 것들 그런걸 만들때 서비스에 코드를 작성합니다. 이러한 비즈니스 로직을 사용할때 데이터가 필요한 시점이 있을 것 입니다. 이를테면 데이터를 쓰거나 읽거나와 같은 이런 일들.. 그러한 것들을 대신해주는 것이 바로 리포지토리 입니다. 리포지토리의 경우 데이터베이스와 직접적으로 상호작용하는 코드를 작성하는 부분입니다. 저장소에서 데이터베이스와 관련된 코드를 관리할 것 이다.

즉, 서비스에는 비즈니스 로직을 작성하고 거기에서 데이터베이스에 접근이 필요할 경우 리포지토리에 작성해둔 데이터베이스 접근 코드를 이용하여 데이터를 조회,수정,삭제를 하는 것이다.

이렇게 보면 서비스와 리포지토리의 차이가 명확하게 보입니다. 근데 실제로 코드로 작성한다고 가정했을 때 다음 같은 상황이 발생한다면 우리는 둘의 차이가 명확하게 무엇인지 구분할 수 없을것이다.

위의 사진을 보면 메세지 서비스와 리포지토리가 가지고 있는 역할이 동일하게 보인다. 이런 단순한 경우 서비스와 리포지토리가 하는 역할의 차이점이 보이지 않는다. 그러나 하나의 서비스에서 여러 레포지토리를 참고하는 순간 ? 서비스와 리포지토리가 분리되어 사용되어야 합니다. 이러한 이유때문에 서비스와 리포지토리는 분리되어야 한다.

리포지토리 작성해보기

우리는 리포지토리를 먼저 구성해볼 것이다. 리포지토리에서 서비스를 사용하기 때문에 서비스보다 먼저 리포지토리를 구성해주는 것이다.

우리의 리포지토리는 로컬 파일을 통해 데이터를 가져오는 방식으로 구현될 예정이다. 리포지토리를 구현하기에 앞서, 우리가 가져올 데이터의 형태를 간단하게 잡고 시작해보자.

{
  "12": {
    "content": "hi there! ",
    "id": 12
  },
  "13": {
    "content": "hi there! ",
    "id": 13
  },
  "14": {
    "content": "hi there! ",
    "id": 14
  }
}

우리가 가져올 데이터의 형태는 이와 같이 구성될 예정이다.

키값으로 가지고 있는 대표값은 id로 설정하고 거기에 id값과 contents가 들어간다. id가 중복이긴 하지만 신경쓰지 않아도 된다.

본격적으로 리포지토리 코드를 작성해보자.

import { read } from 'fs';
import { readFile, writeFile } from 'fs/promises';
//파일을 가지고 작동을 할 것이기 때문에 파일관련 모듈 import 

export class MessagesRepository {
  async findOne(id: string) {
		//id를 이용하여 데이터를 찾는 함수 
    const contents = await readFile('messages.json', 'utf-8');
    const messages = JSON.parse(contents);

    return messages[id];
		//데이터를 구분짓는 값은 id이기 때문에 해당 데이터를 리턴한다
    /*
    {
        12 : {
            id : 12 ,
            content : 'hello'
        }
    }
    */
  }

  async findAll() {
    const contents = await readFile('message.json', 'utf-8');
    const messages = JSON.parse(contents);
    return messages;
		//사용자가 가진 데이터 전부를 리턴하는 함수
		//어떠한 데이터도 필터링 할 필요없이 전부를 리턴한다
  }

  async create(message: string) {
		//입력받은 문자열을 message로 가지는 객체를 생성하는 함수
    const contents = await readFile('messages.json', 'utf-8');
    const messages = JSON.parse(contents);

    const id = Math.floor(Math.random() * 999);
		//id를 랜덤 생성하여 저장하도록 한다

    messages[id] = {
      id,
      message,
    };
		//데이터를 json 으로 저장해 id값을 key값으로 데이터를 삽입한다.

    await writeFile('messages.json', JSON.stringify(messages));

    /*
    {
        12 : {
            id : 12,
            content : 'asdf
        }
    }
    */
  }
}

코드와 관련된 설명은 모두 주석에 있으니 주석을 따라오면서 어떠한 방식으로 작성한건지 생각해보면 된다.

서비스 작성해보기

이렇게 작성한 리포지토리를 쓸 서비스를 작성해보자 !

앞서 말했던바와 같이 서비스와 리포지토리는 비슷한 구성으로 이루어져있다. 이제 한번 코드를 보자.

import { MessagesRepository } from "./messages.repository";

export class MessagesService{

    messageRepo: MessagesRepository;

    constructor(){
        //service is creating it own dependencies
        //서비스는 리포지토리가 없으면 작동하지 않는다
        this.messageRepo = new MessagesRepository();
        //종속성을 자체적으로 생성하는 과정
        //Nest에서는 종속성 주입이라는 것을 통해 설정하기 때문에 실제 앱에서는 이런식으로 코드를 작성하면 안됨
    }

    findOne(id:string){
        //이 부분 때문에 서비스와 리포지토리를 나누는것이 이상해보일 수도 있다.
        return this.messageRepo.findOne(id);
    }

    findAll(){
        return this.messageRepo.findAll();
    }

    create(content: string){
        return this.messageRepo.create(content);
    }

}

보면 constructor을 이용하여 리포지토리를 직접 생성한다. 이러한 과정은 서비스가 리포지토리가 있어야 하기 때문에 필요한데. 이러한 과정을 보통 종속성 주입이라고 한다. 이러한 식으로 작성하는 것은 올바르진 않지만 종속성에 대한 이해를 돕기 위해 직접 종속성을 주입하는 코드로 작성해보았다.

Nest에서는 이것을 종속성 주입 데코레이터를 이용하여 할 것이기 때문에 이후에 수정될 부분이다.

나머지 부분의 경우 리포지토리의 함수를 호출하는 부분이 끝이다. 이러한 코드 때문에 리포지토리와 서비스를 왜 나누냐고 이야기 할 수 있지만 일단, 넘어가보자 !

컨트롤러 작성해보기

우리는 컨트롤러를 작성해볼 것이다. 앞에서 작성했던 서비스와 별반 다를게없다. 그래도 나누는데에는 이유가 있기 때문에 넓은 마음으로 수용해주길 바란다!

import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { createMessagesDto } from './dtos/create-message.dto';
import { MessagesService } from './messages.service';

@Controller('messages') // class decorator
export class MessagesController {
  messagesService: MessagesService;

  constructor() {
    this.messagesService = new MessagesService();
    //실제 서비스를 만들때는 이런식으로 작성하면 안된다.
  }

  @Get() //method decorator
  listMessages() {
    return this.messagesService.findAll();
  }

  @Post()
  createMessages(@Body() body: createMessagesDto /**argument decorator**/) {
    return this.messagesService.create(body.content);
  }
  //들어오는 데이터가 유효하지 않은지 확인하기 위해 pipe를 구현함
  // 유효성 검사 !

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

맨 위에 contructor를 이용하여 직접 메세지 서비스를 선언하여 종속성을 주입하고있다. 저 부분은 이해를 돕기위해 직접 작성한 부분이기 때문에 실제 서비스를 작성할때는 저런식으로 작성하면 안된다.

초반에 컨트롤러를 작성했던 것 처럼 데코레이터를 붙여 어떤 형식으로 데이터를 받을 것인지 설정해주고 만약 필요한 파라미터가 있다면 설정해준다 이를테면 Param 데코레이터를 이용하여 찾을 메세지의 id를 찾은것처럼 말이다.

이제 요청을 해보자.

request.http

### list all messages
GET http://localhost:3000/messages

### Create a new message
POST http://localhost:3000/messages
Content-Type: application/json

{
    "content" : "hihi!"
}

### Get a particultar message
GET http://localhost:3000/messages/321

첫 요청의 결과를 보자 ! 나의 경우 이미 몇번 테스트 했기 때문에 데이터가 있다. 데이터 파일을 작성만 해놓은 상태라면 빈 json {} 만 유지하고 테스트해 볼 것을 권장한다.

json에 있는 모든 데이터를 불러오고있다.

그럼 다음 요청을 테스트해보자 이번 요청은

{
    "content" : "how are you?"
}

라는 메세지를 생성해보려고 한다.

결과는 ?

만약, 메세지를 순수 숫자로 작성하여 보낸다면 다음과 같은 응답이 돌아온다.

이는 앞에서 설정한 파이프라인에서 유효성 검사에 통과하지 못함을 의미한다.

마지막으로 id를 이용하여 데이터를 추출하는 요청이다.

우리는 앞에서 이미 생성되어있던 626번 메세지를 추출할 예정이다.

### Get a particultar message
GET http://localhost:3000/messages/626

응답은 다음과 같이 온다

근데, 우리가 데이터베이스에 존재하지 않는 id 를 부르면 어떻게 될까 ?

### Get a particultar message
GET http://localhost:3000/messages/111

바보같이 이녀석은 200 OK 라는 메세지를 리턴하고 있다.

응답은 오지않지만 데이터베이스에서 반환할 수 없는 id를 받았다면 404와 같은 상태 메세지를 돌려보내야 하는 것이 맞다.

우리는 다음에 이런 문제를 해결해볼것이다.

오류 처리하기

앞에서 언급했던 오류 처리를 코드로 작성해보려고 한다.

우리가 하고싶은 것은 데이터에 없는 id의 정보를 달라고 요청을 받았을 때 해당 데이터가 없음을 에러메세지로 알려주는 것이다.

현재 상태에서는 없다는 것을 알지만 요청이 제대로 왔기 때문에 해당 요청에 대해서 200이라는 정상 sign을 보내고 있다. 이 부분을 수정할 것이다.

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

일단 우리는 NotFountException 이라는 모듈을 임포트해 줄 것입니다.

Nest가 에러를 캐치해 처리하도록 하는 모듈입니다.

그리고 관련 컨트롤러에 함수를 이렇게 수정합니다.

@Get('/:id')
  async getMessages(@Param('id') id: string) {
    const message = await this.messagesService.findOne(id);
    if (!message) {
      throw new NotFoundException('message not found');
    }

    return message;
  }

message에 담겨있는 데이터가 없다 = id로 조회했을때 데이터가 없다와 의미가 같기때문에 분기문을 message의 여부로 잡아서 작성합니다.

만약, 메세지가 없을 경우 notFound라는 예외를 발생시킬 것이고 message not found라는 메세지로 같이 출력할 것입니다.

이런식으로 말이다.

지금까지 우리는 예외처리에 대해서 알아보았다.

다음에는 지금까지 우리가 수동으로 했던 종속성 주입에 대해서 알아 볼 것이다!

profile
반갑습니다 ! 백엔드 개발 공부를 하고있습니다.

0개의 댓글