Layered Architecture Pattern - Repository

윤태규·2024년 1월 18일

01. 저장소 (Repository)

  • 1) 저장소 계층(Repository Layer) 이란? 💡 **저장소 계층(Repository Layer)**은 **데이터 엑세스 계층(Data Access Layer)**이라고도 불리는데요, 주로 **데이터베이스**와 관련된 작업을 처리하는 계층입니다. - **데이터 접근**과 관련된 **세부 사항**을 숨기는 동시에, 메모리상에 **데이터가 존재하는 것처럼** 가정하여 코드를 구현하게 됩니다. - **저장소 계층**을 도입하면, 데이터 저장 방법을 **더욱 쉽게 변경**할 수 있고, 테스트 코드 작성시 **가짜 저장소(Mock Repository)**를 제공하기가 더 쉬워집니다. - 어플리케이션의 **다른 계층**들은 저장소의 **세부 구현 방식**에 대해 알지 못하더라도 해당 기능을 사용할 수 있습니다.즉, **저장소 계층의 변경 사항이 다른 계층에 영향을 주지 않는 것입니다.** → **객체 지향의 개념 중 추상화(Abstraction)와 관계가 있습니다.** - **저장소 계층**은 **데이터 저장소**를 간단히 **추상화**한 것으로, 이 계층을 통해 **모델 계층**과 **데이터 계층**을 명확하게 **분리**할 수 있습니다. - 대표적인 저장소 계층의 메서드 - **`add(), create()`** : 새 원소를 저장소에 **추가합니다.** - **`get(), find()`** : 이전에 추가한 원소를 저장소에서 **가져옵니다.**
  • 2) 저장소 계층의 장단점 저장소 계층의 장점
    • 데이터 모델데이터 처리 인프라에 대한 사항을 분리했기 때문에 단위 테스트(Unit test)를 위한 가짜 저장소(Mock Repository)를 쉽게 만들 수 있습니다.

    • 도메인 모델을 미리 작성하여, 처리해야 할 비즈니스 문제에 더 잘 집중할 수 있게 됩니다.

    • 객체테이블매핑하는 과정을 원하는 대로 제어할 수 있어서 DB 스키마단순화할 수 있다.

    • 저장소 계층ORM을 사용하면 필요할 때 MySQLPostgres와 같은 다른 데이터베이스로 쉽게 전환할 수 있습니다.

      저장소 계층의 단점

    • 저장소 계층이 없더라도 ORM모델저장소결합도를 충분히 완화시켜 줄 수 있습니다.

      → ORM이 없을 때 대부분의 코드는 Raw Query로 작성되어 있기 때문입니다.

    • ORM 매핑수동으로 하려면 개발 코스트가 더욱 소모됩니다.

      → 여기서 설명하는 ORM은 저희가 이전에 사용한 Prisma와 같은 라이브러리를 말합니다.

  • 3) Express로 구현하는 저장소 계층 Repository !https://s3-us-west-2.amazonaws.com/secure.notion-static.com/914029fa-aa47-4ddd-96e1-c41e9d090332/Untitled.png
    • 데이터베이스 관리 (연결, 해제, 자원 관리) 역할을 담당합니다.

    • 데이터베이스의 CRUD 작업을 처리합니다.

      3계층 아키텍처의 마지막 계층인 저장소 계층(Repository Layer)입니다!

      이전에 작성했던 코드에서 서비스 계층(Service Layer)PostsServices에서PostsRepository를 호출하여 데이터요청하는 것을 확인 할 수 있었습니다.

      이번에는 저장소 계층(Repository Layer)이 어떻게 데이터베이스데이터를 가져와 상위 계층에게 반환하는지 확인해보도록 하겠습니다.

    • [코드 스니펫] 3계층 아키텍처 - posts.repository.js

      ```jsx
      // src/repositories/posts.repository.js
      
      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;
        };
      }
      ```

      이번 저장소 계층(Repository Layer)에서는 PostRepository 클래스에서 Prisma의 메소드를 사용해 데이터조회하거나 생성하는 것이 가장 핵심적인 내용입니다.

      💪 이번 예제에서는 단일 테이블만 활용하여 **Prisma**를 사용했기 때문에 코드가 복잡해지지 않았습니다. 그러나, **어플리케이션의 규모가 커지거나**, **데이터베이스의 구성이 복잡**해질 경우에는 저장소 계층의 구조 또한 **복잡**해질 것입니다.

02. [Quiz] 게시글 API 추가하기

  • 1) 게시글 API 추가하기 요구사항

    ❓ **3계층 아키텍처 프로젝트를 아래 요구사항을 이용해 보완해보세요!** - **📚  [3계층 아키텍처] 요구사항 API 명세서** [제목 없는 데이터베이스](https://www.notion.so/66afaf640a9d46578d2db7ee7ca0cbae?pvs=21) - 📒  **[Directory Structure] [3계층 아키텍처] 게시글 API 추가하기** - **[3계층 아키텍처] 프로젝트에서 디렉토리 구조는 변경되지 않습니다.** ``` 내 프로젝트 폴더 이름 ├── package.json ├── prisma │   └── schema.prisma ├── src │   ├── app.js │   ├── controllers │   │   └── posts.controller.js │   ├── middlewares │   │   ├── error-handling.middleware.js │   │   └── log.middleware.js │   ├── repositories │   │   └── posts.repository.js │   ├── routes │   │   ├── index.js │   │   └── posts.router.js │   ├── services │   │   └── posts.service.js │   └── utils │   └── prisma │   └── index.js └── yarn.lock ```
  • 2) 답안 확인하기

  • src/routes/posts.router.js

    // src/routes/posts.router.js
    
    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);
    
    /** 게시글 상세 조회 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;
  • src/controllers/posts.controller.js

    // src/controllers/posts.controller.js
    
    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);
        }
      };
    }
  • src/services/posts.service.js

    // src/services/posts.service.js
    
    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,
        };
      };
    }
  • src/repositories/posts.repository.js

    // src/repositories/posts.repository.js
    
    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,
          },
          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;
      };
    }
profile
끝까지 가자

0개의 댓글