[Node.js] 심화 실습 (6-10) (layered architecture: router - controller - service - repository / Error: schema.prisma not found)
계층형 아키텍처 패턴 프로젝트 템플릿을 다운 받은 후 설계된 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 해보겠음:
아래는 실습/개발 순서대로 작성함.
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;
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);
}
};
}
장점 | 단점 |
- 사용자의 유즈 케이스(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,
};
};
}
장점 | 단점 |
- 데이터 모델과 데이터 처리 인프라에 대한 사항을 분리했기 때문에 단위 테스트(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;
};
}
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로 테스트 해봤는데 전부 잘 됨!