express 프레임워크에서 mvc 패턴 적용해서 만든 코드에서 에러처리 하기

질문 제목이 장황한점 죄송합니다. mvc에 해당하는 model, view, control을 순서대로 코드로 사용해봤습니다.
model:

const Product = require("../models/products");

async function FindOne(id, next) {
    try {
        const product = await Product.findOne({
            where: { id },
        });

        if (product === null) {
            throw new Error("null");
        }

        return product;
    } catch (err) {
        if (err.message === "null") {
            return new Error("no Product");
        } else {
            return new Error(err.message);
        }
    }
}

async function FindAll() {
    try {
        const product = await Product.findAll({});

        if (product === null) {
            throw new Error("null");
        }

        return product;
    } catch (err) {
        if (err.message === "null") {
            return new Error("no Product");
        } else {
            return new Error(err.message);
        }
    }
}

async function getBefore(id) {
    try {
        const BeforeProducts = await Product.findOne({
            where: { id },
        });
        const resultByOldProducts = JSON.stringify(BeforeProducts.dataValues);
        return resultByOldProducts;
    } catch (err) {
        if (
            err.message ===
            "Cannot read properties of null (reading 'dataValues')"
        ) {
            return new Error("no Product");
        } else {
            return new Error(err.message);
        }
    }
}

async function getAfter(id) {
    try {
        const AfterProducts = await Product.findOne({
            where: { id },
        });
        const resultByNewProducts = JSON.stringify(AfterProducts);
        return resultByNewProducts;
    } catch (err) {
        if (
            err.message ===
            "Cannot read properties of null (reading 'dataValues')"
        ) {
            return new Error("no Product");
        } else {
            return new Error(err.message);
        }
    }
}

async function Create(package) {
    try {
        const { id, name, price, origin, type } = package;
        const createdProduct = await Product.create({
            id,
            name,
            price,
            origin,
            type,
        });
        return createdProduct;
    } catch (err) {
        if (err.message === "Validation error") {
            return new Error("same Product");
        } else if (err.message || "notNull Violoation") {
            return new Error("Form Null");
        } else {
            return new Error(err.message);
        }
    }
}

async function Update(package, paramsId) {
    try {
        const { id, name, price, origin, type } = package;
        await Product.update(
            {
                id,
                name,
                price,
                origin,
                type,
            },
            {
                where: { id: paramsId },
            }
        );
        return 0;
    } catch (err) {
        if (err.message === "Validation error") {
            return new Error("same Product");
        } else if (err.message || "notNull Violoation") {
            return new Error("Form Null");
        } else {
            return new Error(err.message);
        }
    }
}

async function Destroy(paramsId) {
    try {
        await Product.destroy({
            where: { id: paramsId },
        });
        return 0;
    } catch (err) {
        return new Error(err.message);
    }
}

module.exports = {
    FindOne,
    FindAll,
    getBefore,
    getAfter,
    Create,
    Update,
    Destroy,
};

view:

const express = require("express");
const controllWorker = require("../controller/productController");
const router = express.Router();

router
    .route("/")
    .get(controllWorker.getProductMain)
    .post(controllWorker.createProduct);
router
    .route("/:id")
    .get(controllWorker.getProductDetail)
    .patch(controllWorker.modifyProduct)
    .delete(controllWorker.removeProduct);

module.exports = router;

control:

const dataWorker = require("../data/productData");

async function getProductDetail(req, res, next) {
    const paramsId = req.params.id;

    const product = await dataWorker.FindOne(paramsId, next);

    if (product.message) {
        return next(product);
    }

    res.locals.id = paramsId;
    res.locals.productName = product.name;
    res.locals.productPrice = product.price;
    res.locals.productOrigin = product.origin;
    res.locals.productType = product.type;
    res.render("productInfo");
}

async function getProductMain(req, res, next) {
    const products = await dataWorker.FindAll();

    if (products.message) {
        return next(products);
    }

    const result = products.map((value, index) => {
        const productNames = [];
        productNames.push(products[index].name);
        console.log(products[index].dataValues);
        return productNames;
    });

    result.shift();

    res.locals.productNames = result;
    res.render("productMain");
}

async function createProduct(req, res, next) {
    const package = req.body;
    const products = await dataWorker.Create(package);

    if (products.message) {
        return next(products);
    }

    const result = JSON.stringify(products);
    const message = "The product has been created";
    res.status(201).render("productCreate", { message, result });
}

async function modifyProduct(req, res, next) {
    const paramsId = req.params.id;
    const package = req.body;

    const getOld = await dataWorker.getBefore(paramsId);

    if (getOld.message) {
        return next(getOld);
    }

    const updater = await dataWorker.Update(package, paramsId);

    if (updater.message) {
        return next(updater);
    }

    const getNew = await dataWorker.getAfter(paramsId);

    if (getNew.message) {
        return next(getNew);
    }

    const message = "The product's info has been modified";

    res.status(200).render("productUpdateDelete", {
        message,
        getOld,
        getNew,
    });
}

async function removeProduct(req, res, next) {
    const paramsId = req.params.id;

    const getOld = await dataWorker.getBefore(paramsId);

    if (getOld.message) {
        return next(getOld);
    }

    const destroyer = await dataWorker.Destroy(paramsId);

    if (destroyer.message) {
        return next(destroyer);
    }

    const getNew = await dataWorker.getAfter(paramsId);

    if (getNew.message) {
        return next(getNew);
    }

    const message = "The product's info has been removed ";

    res.status(203).render("productUpdateDelete", {
        message,
        getOld,
        getNew,
    });
}

module.exports = {
    getProductDetail,
    getProductMain,
    createProduct,
    modifyProduct,
    removeProduct,
};

에러처리 미들웨어를 아래와 같이 사용하고 있습니다.

app.use((err, req, res, next) => {
    console.log(" ### Error Detected! ###");
    if (err.message === "no Product") {
        res.locals.warning = "No applicable products found";
        res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
        res.locals.error.status = 404;
        res.locals.message =
            "The requested product could not be found, please go back.";
        console.error(err);
        return res.render("occasionalError");
    }
    if (err.message === "same Product") {
        res.locals.warning = "A product with the same name exists";
        res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
        res.locals.error.status = 400;
        res.locals.message =
            "The name of the product cannot be the same, so please reset the name";
        console.error(err);
        return res.render("occasionalError");
    }
    if (err.message === "Form Null") {
        res.locals.warning = "One of the forms is not filled in";
        res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
        res.locals.error.status = 400;
        res.locals.message =
            "You forgot to fill out the form. Please check your input";
        return res.render("occasionalError");
    }
    res.locals.message = err.message;
    res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
    res.locals.error.status = 500;
    console.error(err);
    res.render("error");
});

model쪽 코드에서 시퀄라이즈를 이용해서 sql 데이터베이스에 접근하여 데이터를 crud 할 수 있게 되어있습니다. 프로미스를 쉽게 처리하기 위해 async await 문법을 사용했고 오류가 난다면 catch로 로직이흐르게 되어 에러를 new Error()을 사용해서 정의합니다.

문제는 control쪽 코드를 보시면 model과 유사하게 async await 을 사용하여 model쪽에서 리턴한 프로미스 객체를 처리할 수 있게 할 수 있게 해줍니다. 그 대신 await 뒤에는 항상 에러를 리턴받은 것을 대비해서 조건문으로 에러 처리를 하게 해줘야 합니다.

이렇게 조건문이 도배된 이유는 model쪽 코드에서 catch 에서 에러를 한번에 app.js에 존재하는 에러처리 미들웨어로 보내주지 못해서 (model에서 next()를 사용해봤는데 control로 리턴된거 같은 효과가 납니다.) control의 코드는 view에서 모듈로 사용되는 함수들이기 때문에 라우터에서 next(에러 매개변수)를 사용해서 app.js에 있는 에러처리 미들웨어로 한번에 이동이 가능하게 됩니다. 그 대신 매번 조건문을 사용해야 하기 때문에 코드 가독성이 보기 좋지 않습니다. 따라서 control에서 하는 에러처리를 없애고 model의 catch를 통해서 한번에 에러를 app.js에 있는 에러처리 미들웨어로 이동시키는 방법을 알고 싶습니다. 계속해서 프론트앤드 유저분들에게 백앤드 질문을 해서 죄송하지만 만약 이 해결을 아시는분이 계신다면 알려주시면 감사하겠습니다.

// return error와 throw error의 차이

Model에서, Product에서 결과가 없는 경우와 기타 다른 에러들을 핸들링하기 위해 try catch를 쓰셨고, 결과가 없는 경우는 새로운 에러로 감싸서 return 하는 모습입니다.

이후 Control의 modifyProduct에서, getOld는 에러객체일 수도 있고, 결과값일 수도 있습니다.
getOld객체가 에러객체라면, message요소를 가지고 있기 때문에 if(getOld.message)를 통해 에러인지 판단하고 에러핸들러로 넘깁니다.
// if(getOld instanceof Error)라고 쓰는게 더 명확할 것 같네요. :slight_smile:

이 후 updater와 getNew 부분에 대해서도 에러 여부를 판단해야 하기 때문에 같은 if문이 여러개 사용되는 모습입니다.

control에서 에러처리를 없애고 싶으신 이유가 이 부분의 조건문이 반복되기 때문이신 것 같습니다.

return new Error()와 throw new Error()는 명확히 다릅니다.
return new Error()를 사용한 경우 getOld 부분에서 코드가 정상적으로 실행되지만,
throw new Error()를 사용한다면 getOld 부분에서 코드가 멈출 것입니다.

model에서 app.js 핸들러로 바로 넘기는건 mvc 패턴의 구조에 맞지 않아보이네요,
저라면 현재 구조를 유지한 채 다음과 방법을 사용할 것 같습니다.

# Model:

async function FindOne(id, next) {
    try {
        const product = await Product.findOne({
            where: { id },
        });

        if (product === null) {
            throw new Error("no Product");
        }

        return product;
    } catch (err) {
        throw err;
    }
}

#Control

async function modifyProduct(req, res, next) {
    const paramsId = req.params.id;
    const package = req.body;

    try {
        // Model에서 Error가 발생하면 (throw 되면)
        const getOld = await dataWorker.getBefore(paramsId);
        const updater = await dataWorker.Update(package, paramsId);
        const getNew = await dataWorker.getAfter(paramsId);
    } catch (err) {
        // Error Handler로 토스해버린다.
        next(err);
    }

    const message = "The product's info has been modified";

    res.status(200).render("productUpdateDelete", {
        message,
        getOld,
        getNew,
    });
}

답변 감사드립니다. 덕분에 재가 원하던 대로 해결이 되었습니다!

1개의 좋아요