데이터 모델과 데이터 처리 인프라에 대한 사항을 분리했기 때문에 단위 테스트(Unit test)를 위한 가짜 저장소(Mock Repository)를 쉽게 만들 수 있습니다.
도메인 모델을 미리 작성하여, 처리해야 할 비즈니스 문제에 더 잘 집중할 수 있게 됩니다.
객체를 테이블에 매핑하는 과정을 원하는 대로 제어할 수 있어서 DB 스키마를 단순화할 수 있다.
저장소 계층에 ORM을 사용하면 필요할 때 MySQL과 Postgres와 같은 다른 데이터베이스로 쉽게 전환할 수 있습니다.
저장소 계층의 단점
저장소 계층이 없더라도 ORM은 모델과 저장소의 결합도를 충분히 완화시켜 줄 수 있습니다.
→ ORM이 없을 때 대부분의 코드는 Raw Query로 작성되어 있기 때문입니다.
ORM 매핑을 수동으로 하려면 개발 코스트가 더욱 소모됩니다.
→ 여기서 설명하는 ORM은 저희가 이전에 사용한 Prisma와 같은 라이브러리를 말합니다.
데이터베이스 관리 (연결, 해제, 자원 관리) 역할을 담당합니다.
데이터베이스의 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의 메소드를 사용해 데이터를 조회하거나 생성하는 것이 가장 핵심적인 내용입니다.
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;
};
}