6.4Data Access Pattern-Repositories
Repository 패턴으로 데이터에 접근하기
저번 포스팅에서 API를 만들어봤으니, Repository 패턴을 통해 데이터에 접근해보자.
즉, 데이터베이스에 직접 쿼리를 쓰는게 아니라,
UserRepo.find()
처럼 추상화된 메소드를 호출하는 디자인 패턴이다. 이렇게 접근하면 복잡한 쿼리 로직을 클래스에서 편하게 관리가 가능하고, 코드 일관성과 재사용성이 증가하는 이점이 있다.우리의 Repository는 users
테이블에 접근해 아래와 같은 작업을 수행하는 함수들을 포함한다.
Function | 설명 |
---|---|
find | 유저들의 정보를 가지고 있는 객체들의 배열을 반환 |
findById | id를 통해 유저 찾기 |
insert | 유저 정보 추가 |
update | id를 통해 유저 찾고 업데이트하기 |
delete | id를 통해 유저 정보 지우기 |
Repository 만들기
src/repos/user-repo.js
파일을 작성한다. 아래는 우선 find
메소드만 구현한 코드다:
javascriptconst pool = require("../pool"); class UserRepo { static async find() { const { rows } = await pool.query("SELECT * FROM users"); return rows; } static async findById() {} static async insert() {} static async update() {} static async delete() {} } module.exports = UserRepo;
우선 find
만 테스트를 해보자.
users
라우터에 연결
아래는 기존에 작성한 users.js
로, UserRepo.find()
를 GET / users
라우터에 연결한다.
javascriptconst express = require("express"); const UserRepo = require("../repos/user-repo"); const router = express.Router(); router.get("/users", async (req, res) => { const users = await UserRepo.find(); res.send(users); }); router.get("/users/:id", async (req, res) => { }); router.post("/users", async (req, res) => {}); router.put("/users/:id", async (req, res) => {}); router.delete("/users/:id", async (req, res) => {}); module.exports = router;
API 테스트해보기: REST client
npm run start
로 로컬 서버를 시작한다. 이후, 요청을 보내기 위해 postman 또는 VSCode의 REST client 익스텐션을 이용한다. 이번 포스팅에선 후자를 사용한다.
- 우선 익스텐션을 설치하고 프로젝트 루트 폴더에
request.http
라는 파일을 작성한다. - 여기에 요청을 작성한다:
texthttp://localhost:3005/users
- 해당 줄을 우클릭해
send request
를 선택하면 요청이 보내진다.
추가적으로 요청을 보내려면, ###
을 입력하고 아래에 또 다른 경로를 적는다. 우선 이렇게 요청을 보내보면, 현재는 데이터를 넣은게 없으므로 http 200 응답을 받지만 빈 배열([])을 반환한다.
PGAdmin4에서 간단한 데이터를 users
에 넣자.
sqlINSERT INTO users (bio, username) VALUES ('This is my bio', 'Alyson14'), ('This is me', 'Gia67')
이후 요청을 보내보면 아래의 이미지와 같이 우리가 추가한 결과를 반환한다.

여기서, 자바스크립트 코드와 postgresql 간 Casing 전통에 차이가 있을 수 있다. 자바스크립트는 camelCase를 쓰는 반면, postgres에서는 snake_case를 쓰기 때문에 자바스크립트에선 users[0].createdAt
처럼 접근하고 싶겠지만 실제 데이터는 users[0].created_at
으로 접근해야 한다.
데이터베이스에서 데이터를 받아서 자바스크립트 코드로 변환을 하는 입장에서 이 문제는 고치고 가야 이후에 있을 큰 혼란을 방지할 수 있다.
Casing 고치기
쿼리로 데이터를 받고 나서 snake_case를 camelCase로 파싱하는 코드를 작성하면 된다.
javascriptstatic async find() { const { rows } = await pool.query("SELECT * FROM users"); const parsedRows = rows.map((row) => { const replaced = {}; for (const key in row) { const camelCase = key.replace(/([-_][a-z])/gi, ($1) => $1.toUpperCase().replace("_", "") ); replaced[camelCase] = row[key]; } return replaced; }); return parsedRows; }
물론, 이 파서는 find
메소드에서만 쓸 것이 아니므로 utils
같은 폴더에 따로 이 어댑터를 함수로 저장해서 재사용하는 것이 좋다.
to-camel-case.js
javascriptmodule.exports = (rows) => { return rows.map((row) => { const replaced = {}; for (let key in row) { const camelCase = key.replace(/([-_][a-z])/gi, ($1) => $1.toUpperCase().replace("_", "") ); replaced[camelCase] = row[key]; } return replaced; }); };
user-repo.js
javascriptconst pool = require("../pool"); const toCamelCase = require("./utils/to-camel-case"); class UserRepo { static async find() { const { rows } = await pool.query("SELECT * FROM users"); return toCamelCase(rows); } // 다른 메소드... } module.exports = UserRepo;
이후 요청을 보내면 createdAt, updatedAt 식으로 데이터가 들어온다.
id로 유저 찾기
우선 users.js
를 수정한다.
javascriptrouter.get("/users/:id", async (req, res) => { const { id } = req.params; const user = await UserRepo.findById(id); if (user) { res.send(user); } else { res.sendStatus(404); } });
다음으로, user-repo.js
도 수정한다.
javascriptstatic async findById(id) { // WARNING: 큰 보안 문제 있음!! 다음 포스팅에서 수정 예정 const { rows } = await pool.query(` SELECT * FROM users WHERE id = ${id}; `); return toCamelCase(rows)[0]; }
다만, 코드에도 적혀있지만 이 방식은 안전하지 않은 방식이므로 수정이 필요하다.
PostgreSQL의 보안 관련해서는 다음 포스팅에서 보다 자세히 알아보자.