드디어 아기다리고기다린 리소스 문제가 해결이 되어 개발에 착수할 수 있게 되었다. 램 32기가, 하드 10TB 를 중고로 구매하여 물리서버를 구축하였다. 원래의 계획은 MSA 설계 및 각 프로젝트에 필요한 서비스를 직접 올리는 것이었으나, 리소스 문제로 개발 기간이 너무 지체되어 서비스를 구축하는 방법에 대해선 설명으로 듣고 백엔드 개발에 집중하기로 하였다.
1. 파일 구조
처음 node 에서 express 와 socket.IO 를 연습하기위해 파일 구조를 간단하게 만들었었다. 하지만 이제는 직접적으로 서비스가 늘어날 예정이기 때문에 파일 구조를 잘 정의해야 할 것 같다는 판단에 파일 구조를 정리하였다.
chat/
├── .idea/ # IntelliJ IDEA 설정 폴더
├── node_modules/ # npm 모듈 폴더
├── public/ # 정적 파일이 위치하는 폴더
│ ├── client.js # 클라이언트 JavaScript 파일
│ ├── index.html # 메인 HTML 파일
│ └── main.css # 메인 CSS 파일
├── src/ # 소스 코드 폴더
│ ├── routes/ # 라우트 정의 폴더
│ │ ├── chatRoom.js # 채팅방 관련 라우트 정의 파일
│ │ └── index.js # 라우트 엔트리 포인트 파일
│ ├── utils/ # 유틸리티 파일 폴더
│ │ ├── events.js # 이벤트 관련 유틸리티 파일
│ │ ├── format.js # 데이터 포맷 관련 유틸리티 파일
│ │ ├── scyllaDBConfig.js # ScyllaDB 설정 파일
│ │ └── swaggerConfig.js # Swagger 설정 파일
│ └── server.js # Express 서버 설정 및 실행 파일
├── .env # 환경 변수 설정 파일
├── .gitignore # Git 무시 파일 목록
├── bun.lockb # Bun 패키지 매니저 락 파일
├── package.json # 프로젝트 종속성 및 스크립트 정의
└── README.md # 프로젝트 설명 파일
server.js 에서 routes 파일에 있는 파일들을 라우팅 시켜주어 사용할 수 있게끔 하였고, 정적파일은 따로 관리하여 제공하기로 한다.
-> 추후 프론트가 제공된다면 public 에 속해 있는 정적 파일들은 삭제될 예정
2. cassandra-driver 와 dotenv 설치 및 설정
2.1 설치
scyllaDB 와 예민한 정보를 가지고 있는 .env 를 사용하기 위해 먼저 설치를 해주도록 한다.
$bun install cassandra-driver
$bun install dotenv
2.2 설정
scyllaDBConfig.js 파일을 생성후에 다음과 같은 코드를 작성하였다. 여기서 특이한 점은 Spring 또는 Spring Boot 의 경우 mybatis 와 같이 DB 를 연결할 때 , jdbc:postgresql://127.0.0.1:5432 와 같이 설정하는데 Cassandra 드라이버의 경우 IP 주소와 포트번호를 배열 형식으로 입력받아야 정상적으로 작동한다. 예를들어 127.0.0.1:9042 의 경우에 [127.0.0.1:9042] 와 같이 배열 형식으로 되어야 한다.
const cassandra = require('cassandra-driver');
require ('dotenv').config(); // .env 파일 로드
const contactPoints = [process.env.SCYLLA_CONTACT_POINTS];
const localDataCenter = process.env.SCYLLA_LOCAL_DATACENTER;
const keyspace = process.env.SCYLLA_KEYSPACE;
const username = process.env.SCYLLA_SCYLLA_USERNAME;
const password = process.env.SCYLLA_SCYLLA_PASSWORD;
const client = new cassandra.Client({
contactPoints: contactPoints,
localDataCenter: localDataCenter,
keyspace: keyspace,
authProvider: new cassandra.auth.PlainTextAuthProvider(username,password)
});
client.connect()
.then(()=> console.log('Connected to ScyllaDB successfully'))
.catch(err => console.error('Error connecting to ScyllaDB:', err));
module.exports = client;
2.3 테스트
node server.js 를 통해 확인해본 결과 성공적으로 연결 된 것을 확인할 수 있다.
3. 채팅방 API 구현
앞서 작성하였던 API 명세서를 참고하여 채팅방에 필요한 API 를 구현하였다.
3.1 채팅방 리스트
userID 를 통해 해당 유저가 속해 있는 채팅방의 리스트를 출력한다.
router.get('/chatroom/list', async (req, res) => {
const { userId } = req.query;
if ((!userId)) {
return res.status(400).send({ error : 'userID is missing'});
}
try{
const query = 'SELECT room_id FROM room_members WHERE user_id = ?';
const result = await client.execute(query, [userId], {prepare: true});
res.status(200).json(result.rows);
} catch (err) {
res.status(500).send('Error fetching chat rooms');
}
})
3.2 채팅방 생성
채팅방 생성시에 유저를 초대하여 생성하게 된다. 채팅방은 개인방, 단체방 두 종류로 나누어 질 수 있음을 고려하여 설계하였다. 또한 채팅방 생성시 방이름을 작성하지 않으면 초대하는 유저의 이름을 딴 방의 이름으로 들어가게 설계하였다.
ScyllaDB 의 경우 일부 RDMBS 의 특징을 가지고 있는 NoSQL 이기 때문에 JOIN, CASSCADE 와 같은 기능이 없다. 따라서 데이터 삽입시 동시에 진행해야 하는 특이점을 가지고 있다.
router.post('/chatroom/create', async (req, res) => {
let { roomName, userId } = req.body;
const roomId = cassandra.types.Uuid.random();
const timeStamp = new Date();
if (!roomName || roomName.trim().length === 0) {
roomName = `${userId}'s room`;
}
try{
const createRoomQuery = `INSERT INTO chat_rooms (room_id, room_name, created_at) VALUES (?, ?, ?)`;
await client.execute(createRoomQuery, [roomId, roomName, timeStamp], {prepare: true});
const userInsertPromises = userId.map(user =>{
const addMemberQuery = `INSERT INTO room_members (room_id, user_id, joined_at) VALUES (?, ?, ?)`;
return client.execute(addMemberQuery, [roomId, user, timeStamp], {prepare: true});
});
await Promise.all(userInsertPromises);
res.status(201).send({message: 'Chat room created successfully', roomId, roomName, userId});
} catch (err) {
console.error(err);
res.status(500).send('Error creating chat room');
}
})
3.3 채팅방 나가기
특정 유저가 특정 방을 나갈 수 있게 끔 하였으며, 만약에 특정 방에 유저가 모두 나가게 될 경우 해당 방은 삭제하도록 설계하였다.
router.delete('/chatroom/exit', async (req, res) =>{
const { roomId, userId } = req.body;
if (!roomId || !userId) {
return res.send(400).send('roomId or userId are missing');
}
try{
const deleteMemberQuery = `DELETE FROM room_members WHERE room_id = ? AND user_id = ?`;
await client.execute(deleteMemberQuery, [roomId, userId], {prepare: true});
const checkMemberQuery = `SELECT COUNT(*) FROM room_members WHERE room_id = ?`;
const members = await client.execute(checkMemberQuery, [roomId], {prepare: true});
if (members.rows[0].count === '0'){
const deleteRoomQuery = `DELETE FROM chat_rooms WHERE room_id = ?`;
await client.execute(deleteRoomQuery, [roomId], {prepare: true});
}
res.status(200).send({message: `User ${userId} left the room ${roomId}`});
} catch (err){
console.error(err);
res.status(500).send('Failed to exit the room');
}
})
3.4 채팅방 초대하기
이미 생성된 방에 유저를 초대하는 기능을 구현하였다.
router.post('/chatroom/invite', async (req, res) => {
const { roomId, userId } = req.body;
if (!roomId || !userId) {
return res.send(400).send('roomId or userId are missing');
}
try{
const joinRoomQuery = `INSERT INTO room_members (room_id, user_id, joined_at) VALUES (?, ?, ?)`;
await client.execute(joinRoomQuery, [roomId, userId, new Date()], {prepare: true});
res.status(200).send({message: `User ${userId} joined the room ${roomId}`});
} catch (err){
console.error(err);
res.status(500).send('Failed to join the room');
}
})
3.5 방 이름 변경
PATCH 를 통해 방이름만 변경될 수 있도록 구현하였다.
router.patch('/chatroom/update', async (req, res) => {
const { roomId, roomName } = req.body;
if (!roomId || roomName.trim().length === 0) {
return res.send(400).send('roomId is missing');
}
try{
const updateRoomQuery = `UPDATE chat_rooms SET room_name = ? WHERE room_id = ?`;
await client.execute(updateRoomQuery, [roomName, roomId], {prepare: true});
res.status(200).send({message: `Room ${roomId} updated successfully`});
} catch (err){
console.error(err);
res.status(500).send('Failed to update the room');
}
})
'Project > ST00CK' 카테고리의 다른 글
node.js 에서 Kafka 설정하기 (0) | 2025.01.12 |
---|---|
node.js 에서 gRPC 설정하기 (0) | 2025.01.08 |
ScyllaDB ERD 작성, API 명세서 작성 (1차) (0) | 2024.12.31 |
Github 연동 및 Swagger 설치 (2) | 2024.12.16 |
RKE2 Rancher 설치 실패 및 GCP 무료 크레딧 만료 ... 추후 계획 (2) | 2024.12.16 |