본문 바로가기
Backend/Backend 프로젝트

[클론 코딩] 스카이 스캐너(Skyscanner) 클론 코딩 - 서비스 구조화2

by 천우산__ 2023. 6. 7.

지난 글에서 서비스 구조화에 대해서 정리했고, 이번에는 내가 구현하고 싶은 서비스를 구조화 하는 작업을 진행했다.

먼저, api/ 를 포함한 요청이 들어오는 경우를 받아주기 위해 app.js에서 app.use를 이용해 라우터를 등록해주었다.

// app.js

const express = require("express");
const app = express();

const flightsRouter = require("./routes/flightsRouter"); // 내가 구현한 api router
app.use('/api', [flightsRouter]); // '/api'를 포함한 요청이 들어오면, 등록된 router 안에서 찾아서 제공

실행 파일에 내가 만든 서비스 router 를 넣어주었으니, 이번에는 routes 폴더 내 flightsRouter의 내용,

어떤 url 요청에 대해서 어떤 식으로 처리할 것인가에 대해서 작성했다.

// routes/filghtsRouter.js

const epxress = require("express");
const router = epxress.Router();

const FlightsController = require("../controllers/flightsControllers"); // 컨트롤러 호출
const flightsController = new FlightsController(); // 컨트롤러 생성

// 각 url 요청에 대해 각 컨트롤러의 로직을 사용한다고 정의
router.get('/airports', flightsController.findAirports);
router.get('/flights', flightsController.findFlights);
router.get('/newflights', flightsController.createFlights);

module.exports = router;

위의 코드에서, 각 url 요청에 대해서 컨트롤러의 어떤 함수를 사용할 것인가에 대한 내용을 정의하고 있다.

ex) /airports 로 get 요청이 들어오면, 컨트롤러의 findAirports 함수를 실행하도록 한다.

router는 요청이 들어오면 컨트롤러 실행에 대한 내용을 작성했다는 것을 확인했으니, 이번에는 Controllers에 대해서 알아보자

// controllers/flightsService.js

const FlightsServeice = require("../services/flightsService"); // service 파일 불러오기

class FlightsController {
    flightsServeice = new FlightsServeice();

    findAirports = async(req, res, next) => {
        const airports = await this.flightsServeice.findAirports();
        res.status(200).json({ data: airports });
    };

    findFlights = async(req, res, next) => {
        const { sairport_id, eairport_id, start_datetime, people_num, sort_field, sort_by } = req.query;
        const flights = await this.flightsServeice.findFlights(sairport_id, eairport_id, start_datetime, people_num, sort_field, sort_by);

        res.status(200).json({ data: flights });
    };

    createFlights = async(req, res, next) => {
        const { sairport_id, eairport_id, start_datetime } = req.body;
        const newFlights = await this.flightsServeice.createFlights(sairport_id, eairport_id, start_datetime);
        res.status(200).json({ data: newFlights });
    }
}

module.exports = FlightsController;

컨트롤러 파일을 살펴보면, service 파일을 불러와, 새로운 객체를 만들고, 변수 별로 해당 객체의 함수를 실행한다.

바로 이 부분이 router에 요청이 들어오면 요청을 처리하는 과정의 시작인 셈이다. 

여기에서 각 request 의 query 혹은 body 에서 데이터를 받아 서비스 함수에 파라미터를 넣어 요청을 보낸다.

이제, service 파일 내 작성 내용을 확인해보면,

// servivces/flughtsServices.js

const FlightsRepository = require("../repositories/flightsRepositories");

class FlightsService {
    flightsRepository = new FlightsRepository();
	
    findAirports = async() => {
        const airports = await this.flightsRepository.findAirports();
        return airports;
    };

    findFlights = async(sairport_id, eairport_id, start_datetime, people_num, sort_field, sort_by) => {
        let flights = await this.flightsRepository.findFlights(sairport_id, eairport_id, start_datetime, people_num, sort_field, sort_by);

        if (flights.length === 0) {
            await this.flightsRepository.createFlights(sairport_id, eairport_id, start_datetime);
        }

        flights = await this.flightsRepository.findFlights(sairport_id, eairport_id, start_datetime, people_num, sort_field, sort_by);
        return flights;
    };

    createFlights = async(sairport_id, eairport_id, start_datetime) => {
        const newFlights = await this.flightsRepository.createFlights(sairport_id, eairport_id, start_datetime);
        return newFlights;
    };
};

module.exports = FlightsService;

위의 파일에서는 Controller 파일과 마찬가지로 Repository 파일을 불러와 새로운 객체를 만들고,

각 변수에 따라서 해당 객체의 특정 함수를 실행한다. 얼핏 보면 controller의 파일 내용과 비슷해보이지만, 이 파일에서만 수행하는 부분은

실제 서비스 내용을 표현하고 있다는 점이다.

예를 들어, findFlights 를 보면 요청이 들어왔을 때, flightsRepository의 findFlights를 실행하고 findFlights 변수에 저장한다.

그 후 findFlights 변수의 길이가 0이라면, createFlights 를 수행한 다음, 다시 findFlights 를 실행 후 반환한다.

위의 사유는 아래에서 자세히 설명한다.

마지막으로, service가 요청을 보내고 있는, repositories 파일을 확인해보자.

// repositories/flightsRepositories.js

const { Flights, Airports } = require("../models");
const { Op } = require("sequelize");
const makeRandom = require("../utils/mkRandomNum");
const makeDate = require("../utils/stringToDate");
const govApi = require("../utils/govApi");


class FlightsRepository {

    findAirports = async() => {
        try {
            const airports = await Airports.findAll({
                attributes: ["airport_id", "airport_city", "airport_code"]
            });
            return airports

        } catch (error) {
            const errorMessage = "공항 정보 조회에 실패했습니다.";
            return errorMessage;
        }
    };

    // db에서 해당 조건에 맞는 데이터 있는지 확인
    findFlights = async(sairport_id, eairport_id, start_datetime, people_num, sort_field, sort_by) => {
        try {
            const newDate = new makeDate(start_datetime);
            const newDate2 = new makeDate(start_datetime);

            const startDate = newDate.mkDate();
            const afterOneDate = newDate2.mkDate();

            afterOneDate.setDate(afterOneDate.getDate() + 1);

            const field = sort_field === "price" ? "price" : "start_datetime";
            const order = sort_by === "asc" ? "ASC" : "DESC";

            const flights = await Flights.findAll({
                attributes: ["flight_id", "flight_num", "company", "sairport_id", "eairport_id",
                    "start_datetime", "end_datetime", "price", "seat_left"
                ],
                where: {
                    [Op.and]: [
                        { sairport_id },
                        { eairport_id },
                        {
                            start_datetime: {
                                [Op.gte]: startDate
                            }
                        },
                        {
                            start_datetime: {
                                [Op.lt]: afterOneDate
                            }
                        },
                        {
                            seat_left: {
                                [Op.gte]: people_num
                            }
                        }
                    ]
                },
                include: [{
                    model: Airports,
                    attributes: ["airport_id", "airport_city", "airport_code"],
                    as: "start_airport"
                }, {
                    model: Airports,
                    attributes: ["airport_id", "airport_city", "airport_code"],
                    as: "end_airport"
                }],
                order: [
                    [field, order]
                ]
            });
            return flights;

        } catch (error) {
            const errorMessage = "DB 정보 조회 실패";
            return errorMessage;
        }
    };

    // // 없으면 API로 호출 후 저장, 좌석 수 1 ~ 50석 랜덤 배치
    createFlights = async(sairport_id, eairport_id, start_datetime) => {
        try {

            // ID로 받아 Code로 치환
            const startCityName = await Airports.findOne({
                attribute: ["airport_city"],
                where: { airport_id: sairport_id }
            });

            const endCityName = await Airports.findOne({
                attribute: ["airport_city"],
                where: { airport_id: eairport_id }
            });

            // req 문자로 받으나, 조회는 Number로 해야함.
            const start_date = Number(start_datetime.substr(0, 8));

            // 공공 데이터 포털에서 가져오기
            let res = await govApi(startCityName.dataValues.airport_code, endCityName.dataValues.airport_code, start_date);
            let data = res.data.response.body.items.item;

            let message = '해당 기준의 운행 정보는 존재하지 않습니다.';
            if (data !== undefined) {

                for await (let d of data) {
                    // 남은 자리, 비용 난수 생성
                    const seatLeft = makeRandom(1, 50);
                    const price = makeRandom(100, 500) * 1000;

                    const { arrAirportNm, depAirportNm, arrPlandTime, depPlandTime } = d;

                    // 이름 기준으로 공항 ID 찾기
                    const depAirportId = await Airports.findOne({
                        attribute: ["airport_id"],
                        where: { airport_city: depAirportNm }
                    });

                    const arrAirportId = await Airports.findOne({
                        attribute: ["airport_id"],
                        where: { airport_city: arrAirportNm }
                    });

                    // response 데이터 에서 공항 ID 찾기
                    const startAiportId = depAirportId.dataValues.airport_id;
                    const endAiportId = arrAirportId.dataValues.airport_id;

                    // 
                    const start_time = new makeDate(String(depPlandTime));
                    const end_time = new makeDate(String(arrPlandTime));

                    await Flights.create({
                        flight_num: d.vihicleId,
                        sairport_id: startAiportId,
                        eairport_id: endAiportId,
                        company: d.airlineNm,
                        start_datetime: start_time.mkDatetime(),
                        end_datetime: end_time.mkDatetime(),
                        price,
                        seat_left: seatLeft
                    });
                };
                message = "공공 데이터 API 기반 데이터 생성 완료";
            };

            return message;

        } catch (error) {
            console.log(error)
            const errorMessage = "공공 정보 API 호출 오류";
            return errorMessage;
        }
    };
};

module.exports = FlightsRepository;

해당 파일은 DB와 통신하는 로직이 작성된 코드들이다. 조회, 생성, 수정 등의 요청을 이 곳으로 모아서 관리한다.이런 식으로 서비스를 구성하는 요소별로 나눠서 관리하면, 수정이 필요하거나 문제가 생겼던 부분만 집중적으로 보고 수정할 수 있다.

  1. url 요청이 들어왔을때 처리하는 방법을 정의하는 파트
  2. 요청 정보(query, body)를 식별하여 서비스 로직으로 전달하는 파트
  3. 서비스 흐름을 구현하고 리포지토리로 전달하는 파트
  4. 요청에 따라 db 입출력하는 파트

구조화와는 관련이 없지만 서비스 로직에서 findFlights 실행 시 길이를 검사하는 사유는 아래와 같다.

createFlights 로직에는 공공데이터 API 를 사용하고있는데, 해당 API 결과값 내에는 특정 필드 (seat_left)가 없다.

이 부분을 랜덤한 수로 규정해야하는데, 매번 모든 운항 정보를 검색하고, 데이터를 넣어주기에는 부담이 있었다.

(테스트 결과에 따라서 서버를 끄고 킬 일이 많았기 때문에)

그래서 생각한 방법은, DB를 비워두고, 요청이 발생하면 DB에서 값을 찾는다.

유저가 요청한 정보가 DB에 없으면

1. 서버에서 공공데이터 API를 통해 요청을 하고, 반환된 값에 필드를 추가하여 DB에 저장하고, 저장된 DB를 보여준다.

1-1. 다른 누군가 이미 API로 한번 검색한 데이터를 요청하는 경우, DB에서 전달한다.

2. 서버에서 공공데이터 API를 통해 요청을 했는데, 반환된 값이 없다면, "운항정보 없음" 메세지를 반환한다.

위의 케이스를 가정하고 서비스를 구현했고, 해당 내용을  flightsServices.js 파일에 적용했다.