상세 컨텐츠

본문 제목

[Node.js] 심화 실습 (6-10) (layered architecture: router - controller - service - repository / Error: schema.prisma not found)

notes

by 서울의볼 2024. 2. 19. 18:55

본문

계층형 아키텍처 패턴 프로젝트 템플릿을 다운 받은 후 설계된 prisma schema를 npx prisma db push하고자 하였으나 하기의 에러 메세지가 나왔음:

"Error: Could not find a schema.prisma file that is required for this command."

그래서 조금의 구글링을 통해 아래와 같이 schema 파일 위치를 직접 지정하여 push를 다시 시도하였으나, 이번엔 환경변수를 찾을 수 없다고 나옴:

또 열심히 찾아보니, db push 시 default로 root directory에서 .env 파일을 찾아 환경변수를 참조한다고 함. 근데 해당 파일은 template 파일 내 실습파일이 있던터라 reference error가 계속 났었던 것임.

 

template 내 실습파일을 꺼내 root directory로써 재차 명령어를 기입하니 잘 되었음.

 

본격적으로 실습에 들어가기 전 전 글의 아키텍처 패턴을 remind 해보겠음:

아래는 실습/개발 순서대로 작성함.

Router

  • 우선 routes 파일 내 posts.router에서 기존의 async(req, res, next) 콜백함수가 아닌 controller를 refer하여 controller에서 request를 받고 서버에서 처리된 결과를 반환(response)하도록 연결함.
    • import express from "express";
      import { PostsController } from "../controllers/posts.controller.js";
      
      const router = express.Router();
      
      // PostsController의 인스턴스를 생성합니다.
      const postsController = new PostsController();
      
      /** 게시글 조회 API **/
      router.get("/", postsController.getPosts); // 기존의 async(res, req, next)와 같은 콜백함수 X
      /** 게시글 작성 API **/
      router.post("/", postsController.createPost);
      
      export default router;​

Controller 계층

  • 이후, posts.controller로 가서 app.js에서 명시했듯이 에러핸들링 미들웨어를 위한 try catch 부터 설정함.
    • posts.controller에선 service 계층을 조회하게 됨:
    • import { PostsService } from "../services/posts.service.js";
      
      // Post의 컨트롤러(Controller)역할을 하는 클래스
      export class PostsController {
        postsService = new PostsService(); // Post 서비스를 클래스를 컨트롤러 클래스의 멤버 변수로 할당합니다.
      
        /** 게시글 조회 API **/
        getPosts = async (req, res, next) => {
          try {
            // 서비스 계층에 구현된 findAllPosts 로직을 실행합니다.
            const posts = await this.postsService.findAllPosts();
      
            return res.status(200).json({ data: posts });
          } catch (err) {
            next(err);
          }
        };
      
        /** 게시글 작성 API **/
        createPost = async (req, res, next) => {
          try {
            const { nickname, password, title, content } = req.body;
      
            // 서비스 계층에 구현된 createPost 로직을 실행합니다.
            const createdPost = await this.postsService.createPost(
              nickname,
              password,
              title,
              content
            );
      
            return res.status(201).json({ data: createdPost });
          } catch (err) {
            next(err);
          }
        };
      }
    • await this.postsService.findAllPosts() 부연설명 : PostsController 클래스의 postService 인스턴스에서 findAllPost 메서드를 호출하는 것. 컨트롤러는 하위 계층의 내부 구조에 대해 신경 안씀. 대신, 외부에 공개된 메서드를 호출 할 뿐. 이것이 가능한 이유는 추상화(Abstraction)의 특성 때문임.
    • PostsController 클래스는 전달된 요청(Request)을 처리하기 위해 PostsService를 호출하도록 구현함. 여기서 컨트롤러가 비즈니스 로직을 직접 수행하지 않고, 클라이언트의 요청을 서비스 계층으로 바로 전달 하도록 구현한 것을 확인 할 수 있음.

Service 계층

    • 서비스 계층(Service Layer): 다른 이름으로는 비즈니스 로직 계층(Business logic layer)은 아키텍처의 가장 핵심적인 비즈니스 로직을 수행하고 클라이언트가 원하는 요구사항을 구현하는 계층. 이놈은 프레젠테이션 계층(Presentation Layer)데이터 엑세스 계층(Data Access Layer) 사이에서 중간 다리 역할을 하며, 서로 다른 두 계층이 직접 통신하지 않게 만들어줌.
    • 장단점(왠지 면접 같은데 많이 나올 것만 같이 생김):
    • 장점 단점
      - 사용자의 유즈 케이스(Use Case)워크플로우(Workflow)를 명확히 정의하고 이해할 수 있도록 도와줌
      - 비즈니스 로직이 API 뒤에 숨겨져 있으므로, 서비스 계층의 코드를 자유롭게 수정하거나 리팩터링할 수 있음
      - 저장소 패턴(Repository Pattern)가짜 저장소(Fake Repository)와 조합하면 높은 수준테스트작성할 수 있
      - 서비스 계층 또한 다른 추상화 계층이므로, 잘못 사용하면 코드의 복잡성을 증가시킬 수 있음
      - 한 서비스 계층이 다른 서비스 계층의존하는 경우, 의존성 관리가 복잡해질 수 있음
      - 해당 계층에 너무 많은 기능을 넣으면 빈약한 도메인 모델(Anemic Domain Model)과 같은 안티 패턴이 생길 수 있음
  • import { PostsRepository } from "../repositories/posts.repository.js";
    
    export class PostsService {
      postsRepository = new PostsRepository(); // postService를 controller에서 instance화 시켰듯이, respository도 인스턴스화 하여 멤버 변수로 만듦
    
      findAllPosts = async () => {
        // 저장소(Repository)에게 데이터를 요청합니다.
        const posts = await this.postsRepository.findAllPosts();
    
        // 호출한 Post들을 가장 최신 게시글 부터 (내림차순)정렬합니다.
        posts.sort((a, b) => {
          return b.createdAt - a.createdAt;
        });
    
        // 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터(content, pw 제외)를 가공합니다.
        return posts.map((post) => {
          return {
            postId: post.postId,
            nickname: post.nickname,
            title: post.title,
            createdAt: post.createdAt,
            updatedAt: post.updatedAt,
          };
        });
      };
    
      createPost = async (nickname, password, title, content) => {
        // 저장소(Repository)에게 데이터를 요청합니다.
        const createdPost = await this.postsRepository.createPost(
          nickname,
          password,
          title,
          content
        );
    
        // 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
        return {
          postId: createdPost.postId,
          nickname: createdPost.nickname,
          title: createdPost.title,
          content: createdPost.content,
          createdAt: createdPost.createdAt,
          updatedAt: createdPost.updatedAt,
        };
      };
    }​

Repository 계층

    • 저장소 계층(Repository Layer)데이터 엑세스 계층(Data Access Layer)이라고도 불리는데, 주로 데이터베이스와 관련된 작업을 처리하는 계층. 주목할만한 이점 한 가지로, 저장소 계층을 도입하면, 데이터 저장 방법을 더욱 쉽게 변경할 수 있고, 테스트 코드 작성시 가짜 저장소(Mock Repository)를 제공하기가 더 쉬워진다는 것.
      • 저장소 계층데이터 저장소를 간단히 추상화한 것으로, 이 계층을 통해 모델 계층데이터 계층을 명확하게 분리할 수 있음.
      • 대표적인 저장소 계층의 메서드:
        • add(), create() : 새 원소를 저장소에 추가.
        • get(), find() : 이전에 추가한 원소를 저장소에서 가져옴.
      • 장점 단점
        데이터 모델과 데이터 처리 인프라에 대한 사항을 분리했기 때문에 단위 테스트(Unit test)를 위한 가짜 저장소(Mock Repository)를 쉽게 만들 수 있음
        도메인 모델을 미리 작성하여, 처리해야 할 비즈니스 문제에 더 잘 집중할 수 있음
        객체를 테이블에 매핑하는 과정을 원하는 대로 제어할 수 있어서 DB 스키마를 단순화할 수 있음
        저장소 계층에 ORM을 사용하면 필요할 때 MySQL과 Postgres와 같은 다른 데이터베이스로 쉽게 전환할 수 있
        저장소 계층이 없더라도 ORM은 모델과 저장소의 결합도를 충분히 완화시켜 줄 수 있음. ORM이 없을 때 대부분의 코드는 Raw Query로 작성되어 있기 때문
        ORM 매핑을 수동으로 하려면 개발 코스트가 더욱 소모됨. 여기서 설명하는 ORM은 저희가 이전에 사용한 Prisma와 같은 라이브러리를 말함.
      • 구현된 코드:
      • import { prisma } from "../utils/prisma/index.js";
        
        export class PostsRepository {
          findAllPosts = async () => {
            // ORM인 Prisma에서 Posts 모델의 findMany 메서드를 사용해 데이터를 요청합니다.
            const posts = await prisma.posts.findMany();
        
            return posts;
          };
        
          createPost = async (nickname, password, title, content) => {
            // ORM인 Prisma에서 Posts 모델의 create 메서드를 사용해 데이터를 요청합니다.
            const createdPost = await prisma.posts.create({
              data: {
                nickname,
                password,
                title,
                content,
              },
            });
        
            return createdPost;
          };
        }
      • 생성 완료후 반환 값으로 의도한대로 password가 빠진 것 또한 확인할 수 있음:

추가 기능 구현 - 상세 조회 / 수정 / 삭제

    • 여기까지 C(생성)R(조회)를 구현하였고, 추가 기능 구현은 router - controller - service - repository 순으로 함:
    • Router
      import express from "express";
      import { PostsController } from "../controllers/posts.controller.js";
      
      const router = express.Router();
      
      // PostsController의 인스턴스를 생성합니다.
      const postsController = new PostsController();
      
      /** 게시글 조회 API **/
      router.get("/", postsController.getPosts); // 기존의 async(res, req, next)와 같은 콜백함수 X
      /** 게시글 상세 조회 API **/
      router.get("/:postId", postsController.getPostById);
      /** 게시글 작성 API **/
      router.post("/", postsController.createPost);
      /** 게시글 수정 API **/
      router.put("/:postId", postsController.updatePost);
      /** 게시글 삭제 API **/
      router.delete("/:postId", postsController.deletePost);
      
      export default router;
    • Controller
      import { PostsService } from "../services/posts.service.js";
      
      // Post의 컨트롤러(Controller)역할을 하는 클래스
      export class PostsController {
        postsService = new PostsService(); // Post 서비스를 클래스를 컨트롤러 클래스의 멤버 변수로 할당합니다.
      
        getPosts = async (req, res, next) => {
          try {
            // 서비스 계층에 구현된 findAllPosts 로직을 실행합니다.
            const posts = await this.postsService.findAllPosts();
      
            return res.status(200).json({ data: posts });
          } catch (err) {
            next(err);
          }
        };
      
        getPostById = async (req, res, next) => {
          try {
            const { postId } = req.params;
      
            // 서비스 계층에 구현된 findPostById 로직을 실행합니다.
            const post = await this.postsService.findPostById(postId);
      
            return res.status(200).json({ data: post });
          } catch (err) {
            next(err);
          }
        };
      
        createPost = async (req, res, next) => {
          try {
            const { nickname, password, title, content } = req.body;
      
            // 서비스 계층에 구현된 createPost 로직을 실행합니다.
            const createdPost = await this.postsService.createPost(
              nickname,
              password,
              title,
              content
            );
      
            return res.status(201).json({ data: createdPost });
          } catch (err) {
            next(err);
          }
        };
      
        updatePost = async (req, res, next) => {
          try {
            const { postId } = req.params;
            const { password, title, content } = req.body;
      
            // 서비스 계층에 구현된 updatePost 로직을 실행합니다.
            const updatedPost = await this.postsService.updatePost(
              postId,
              password,
              title,
              content
            );
      
            return res.status(200).json({ data: updatedPost });
          } catch (err) {
            next(err);
          }
        };
      
        deletePost = async (req, res, next) => {
          try {
            const { postId } = req.params;
            const { password } = req.body;
      
            // 서비스 계층에 구현된 deletePost 로직을 실행합니다.
            const deletedPost = await this.postsService.deletePost(postId, password);
      
            return res.status(200).json({ data: deletedPost });
          } catch (err) {
            next(err);
          }
        };
      }
    • Service
      import { PostsRepository } from "../repositories/posts.repository.js";
      
      export class PostsService {
        postsRepository = new PostsRepository();
      
        findAllPosts = async () => {
          // 저장소(Repository)에게 데이터를 요청합니다.
          const posts = await this.postsRepository.findAllPosts();
      
          // 호출한 Post들을 가장 최신 게시글 부터 정렬합니다.
          posts.sort((a, b) => {
            return b.createdAt - a.createdAt;
          });
      
          // 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
          return posts.map((post) => {
            return {
              postId: post.postId,
              nickname: post.nickname,
              title: post.title,
              createdAt: post.createdAt,
              updatedAt: post.updatedAt,
            };
          });
        };
      
        findPostById = async (postId) => {
          // 저장소(Repository)에게 특정 게시글 하나를 요청합니다.
          const post = await this.postsRepository.findPostById(postId);
      
          return {
            postId: post.postId,
            nickname: post.nickname,
            title: post.title,
            content: post.content,
            createdAt: post.createdAt,
            updatedAt: post.updatedAt,
          };
        };
      
        createPost = async (nickname, password, title, content) => {
          // 저장소(Repository)에게 데이터를 요청합니다.
          const createdPost = await this.postsRepository.createPost(
            nickname,
            password,
            title,
            content
          );
      
          // 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
          return {
            postId: createdPost.postId,
            nickname: createdPost.nickname,
            title: createdPost.title,
            content: createdPost.content,
            createdAt: createdPost.createdAt,
            updatedAt: createdPost.updatedAt,
          };
        };
      
        updatePost = async (postId, password, title, content) => {
          // 저장소(Repository)에게 특정 게시글 하나를 요청합니다.
          const post = await this.postsRepository.findPostById(postId);
          if (!post) throw new Error("존재하지 않는 게시글입니다.");
      
          // 저장소(Repository)에게 데이터 수정을 요청합니다.
          await this.postsRepository.updatePost(postId, password, title, content);
      
          // 변경된 데이터를 조회합니다.
          const updatedPost = await this.postsRepository.findPostById(postId);
      
          return {
            postId: updatedPost.postId,
            nickname: updatedPost.nickname,
            title: updatedPost.title,
            content: updatedPost.content,
            createdAt: updatedPost.createdAt,
            updatedAt: updatedPost.updatedAt,
          };
        };
      
        deletePost = async (postId, password) => {
          // 저장소(Repository)에게 특정 게시글 하나를 요청합니다.
          const post = await this.postsRepository.findPostById(postId);
          if (!post) throw new Error("존재하지 않는 게시글입니다.");
      
          // 저장소(Repository)에게 데이터 삭제를 요청합니다.
          await this.postsRepository.deletePost(postId, password);
      
          return {
            postId: post.postId,
            nickname: post.nickname,
            title: post.title,
            content: post.content,
            createdAt: post.createdAt,
            updatedAt: post.updatedAt,
          };
        };
      }
    • Repository
      import { prisma } from "../utils/prisma/index.js";
      
      export class PostsRepository {
        findAllPosts = async () => {
          // ORM인 Prisma에서 Posts 모델의 findMany 메서드를 사용해 데이터를 요청합니다.
          const posts = await prisma.posts.findMany();
      
          return posts;
        };
      
        findPostById = async (postId) => {
          // ORM인 Prisma에서 Posts 모델의 findUnique 메서드를 사용해 데이터를 요청합니다.
          const post = await prisma.posts.findUnique({
            where: { postId: +postId },
          });
      
          return post;
        };
      
        createPost = async (nickname, password, title, content) => {
          // ORM인 Prisma에서 Posts 모델의 create 메서드를 사용해 데이터를 요청합니다.
          const createdPost = await prisma.posts.create({
            data: {
              nickname,
              password,
              title,
              content,
            },
          });
      
          return createdPost;
        };
      
        updatePost = async (postId, password, title, content) => {
          // ORM인 Prisma에서 Posts 모델의 update 메서드를 사용해 데이터를 수정합니다.
          const updatedPost = await prisma.posts.update({
            where: {
              postId: +postId,
              password: password, // bcrypt로 hash하면 좋음
            },
            data: {
              title,
              content,
            },
          });
      
          return updatedPost;
        };
      
        deletePost = async (postId, password) => {
          // ORM인 Prisma에서 Posts 모델의 delete 메서드를 사용해 데이터를 삭제합니다.
          const deletedPost = await prisma.posts.delete({
            where: {
              postId: +postId,
              password: password,
            },
          });
      
          return deletedPost;
        };
      }

Insomnia로 테스트 해봤는데 전부 잘 됨!

관련글 더보기