본문 바로가기

개발

Node.js로 알아보는 마이크로서비스 아키텍처

안녕하세요, 오늘은 Node.js와 마이크로서비스 아키텍처에 대해 이야기해보려고 합니다. 마이크로서비스 아키텍처란, 하나의 큰 애플리케이션을 여러 개의 작은 서비스로 나누어 개발하고 운영하는 방식입니다. 각 서비스는 독립적으로 배포하고 확장할 수 있으며, 서로 다른 언어나 프레임워크를 사용할 수 있습니다.

 

마이크로서비스 아키텍처의 장점은 다음과 같습니다.

- 서비스별로 개발 속도와 품질을 향상시킬 수 있습니다.

- 서비스별로 장애를 격리하고 복구할 수 있습니다.

- 서비스별로 자유롭게 기술 스택을 선택하고 변경할 수 있습니다.

- 서비스별로 수요에 따라 자동으로 확장하거나 축소할 수 있습니다.

 

하지만 마이크로서비스 아키텍처에도 단점이 있습니다.

- 서비스 간의 통신과 조정이 복잡해집니다.

- 서비스 간의 일관성과 트랜잭션을 보장하기 어려워집니다.

- 서비스의 수가 많아지면 관리와 모니터링이 어려워집니다.

 

그렇다면 Node.js는 왜 마이크로서비스 아키텍처와 잘 어울리는지 알아보겠습니다. Node.js는 자바스크립트를 기반으로 하는 비동기식, 이벤트 기반의 런타임 환경입니다. Node.js의 특징은 다음과 같습니다.

- 단일 스레드 모델로 CPU 자원을 효율적으로 사용할 수 있습니다.

- 비동기식 I/O 처리로 네트워크 요청을 빠르게 처리할 수 있습니다.

- 이벤트 기반의 프로그래밍 모델로 콜백 함수를 통해 비즈니스 로직을 구현할 수 있습니다.

- npm을 통해 다양한 모듈과 라이브러리를 쉽게 설치하고 관리할 수 있습니다.

 

Node.js는 마이크로서비스 아키텍처와 잘 어울리는 이유는 다음과 같습니다.

- 가볍고 빠르기 때문에 작은 단위의 서비스를 쉽게 개발하고 배포할 수 있습니다.

- 네트워크 요청을 잘 처리하기 때문에 서비스 간의 통신에 적합합니다.

- 자바스크립트를 사용하기 때문에 프론트엔드와 백엔드의 개발 언어를 통일할 수 있습니다.

- npm을 통해 다양한 마이크로서비스 관련 모듈과 라이브러리를 사용할 수 있습니다.

 

그럼 Node.js를 이용하여 간단한 마이크로서비스 아키텍처를 구축해보겠습니다. 예시로, 사용자 인증과 게시글 관리라는 두 개의 서비스를 만들어보겠습니다. 각 서비스는 REST API를 제공하며, MongoDB를 데이터베이스로 사용합니다. 또한, 서비스 간의 통신을 위해 RabbitMQ라는 메시지 브로커 라이브러리를 사용합니다. 다음은 각 서비스의 구조와 기능입니다.

 

사용자 인증 서비스

포트: 3000

엔드포인트: /users, /auth

기능: 사용자 등록, 로그인, 토큰 발급 및 검증

 

게시글 관리 서비스

포트: 3001

엔드포인트: /posts

기능: 게시글 생성, 조회, 수정, 삭제

 

먼저, 사용자 인증 서비스를 만들어보겠습니다. 다음은 서비스의 주요 코드입니다.

// app.js

// 모듈 임포트
const express = require('express');
const mongoose = require('mongoose');
const amqp = require('amqplib/callback_api');
const userRouter = require('./routes/user');
const authRouter = require('./routes/auth');

// 앱 생성
const app = express();

// 미들웨어 설정
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// 라우터 설정
app.use('/users', userRouter);
app.use('/auth', authRouter);

// 에러 핸들러 설정
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ message: err.message });
});

// 데이터베이스 연결
mongoose.connect('mongodb://localhost:27017/auth', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error(err));

// 메시지 브로커 연결
amqp.connect('amqp://localhost', (err, conn) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log('Connected to RabbitMQ');
  // 채널 생성
  conn.createChannel((err, ch) => {
    if (err) {
      console.error(err);
      process.exit(1);
    }
    // 큐 생성 및 바인딩
    ch.assertQueue('user.created', { durable: true });
    ch.assertQueue('user.deleted', { durable: true });
    ch.bindQueue('user.created', 'user', 'created');
    ch.bindQueue('user.deleted', 'user', 'deleted');
    // 채널을 앱에 추가
    app.set('ch', ch);
  });
});

// 서버 시작
app.listen(3000, () => console.log('Server listening on port 3000'));
// routes/user.js

// 모듈 임포트
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/user');

// 라우터 생성
const router = express.Router();

// 사용자 등록 라우트
router.post('/', async (req, res, next) => {
  try {
    // 요청 바디에서 이메일과 비밀번호를 가져옴
    const { email, password } = req.body;
    // 이메일이 이미 존재하는지 확인
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      // 이미 존재하면 에러 발생시킴
      const error = new Error('Email already exists');
      error.status = 409;
      throw error;
    }
    // 비밀번호를 해싱함
    const hashedPassword = await bcrypt.hash(password, 12);
    // 새로운 사용자를 생성함
    const user = new User({
      email,
      password: hashedPassword,
    });
    // 데이터베이스에 저장함
    await user.save();
    // 메시지 브로커에 사용자 생성 이벤트를 전송함
    const ch = req.app.get('ch');
    ch.sendToQueue('user.created', Buffer.from(JSON.stringify(user)), { persistent: true });
    // 응답으로 사용자 정보를 보냄 (비밀번호 제외)
    res.status(201).json({ _id: user._id, email: user.email });
  } catch (err) {
    next(err);
  }
});

// 사용자 삭제 라우트 (관리자용)
router.delete('/:id', async (req, res, next) => {
  try {
    // 요청 파라미터에서 사용자 아이디를 가져옴
    const { id } = req.params;
    // 데이터베이스에서 사용자를 찾음
    const user = await User.findById(id);
    if (!user) {
      // 사용자가 없으면 에러 발생시킴
      const error = new Error('User not found');
      error.status = 404;
      throw error;
    }
    // 데이터베이스에서 사용자를 삭제함
    await user.remove();
    // 메시지 브로커에 사용자 삭제 이벤트를 전송함
    const ch = req.app.get('ch');
    ch.sendToQueue('user.deleted', Buffer.from(JSON.stringify(user)), { persistent: true });
    // 응답으로 삭제된 사용자 정보를 보냄 (비밀번호 제외)
    res.status(200).json({ _id: user._id, email: user.email });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

 

이렇게 사용자 인증 서비스를 만들었습니다. 다음으로, 게시글 관리 서비스를 만들어보겠습니다. 다음은 서비스의 주요 코드입니다.

// app.js

// 모듈 임포트
const express = require('express');
const mongoose = require('mongoose');
const amqp = require('amqplib/callback_api');
const postRouter = require('./routes/post');

// 앱 생성
const app = express();

// 미들웨어 설정
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// 라우터 설정
app.use('/posts', postRouter);

// 에러 핸들러 설정
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ message: err.message });
});

// 데이터베이스 연결
mongoose.connect('mongodb://localhost:27017/post', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error(err));

// 메시지 브로커 연결
amqp.connect('amqp://localhost', (err, conn) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log('Connected to RabbitMQ');
  // 채널 생성
  conn.createChannel((err, ch) => {
    if (err) {
      console.error(err);
      process.exit(1);
    }
    // 큐 생성 및 바인딩
    ch.assertQueue('user.created', { durable: true });
    ch.assertQueue('user.deleted', { durable: true });
    ch.bindQueue('user.created', 'user', 'created');
    ch.bindQueue('user.deleted', 'user', 'deleted');
    // 큐에서 메시지를 받아서 처리하는 함수 정의
    const handleUserCreated = (msg) => {
      // 메시지를 JSON 형식으로 파싱함
      const user = JSON.parse(msg.content.toString());
      // 콘솔에 로그 출력함
      console.log(`User created: ${user.email}`);
      // 메시지를 처리했다고 알림
      ch.ack(msg);
    };
    const handleUserDeleted = (msg) => {
      // 메시지를 JSON 형식으로 파싱함
      const user = JSON.parse(msg.content.toString());
      // 콘솔에 로그 출력함
      console.log(`User deleted: ${user.email}`);
      // 메시지를 처리했다고 알림
      ch.ack(msg);
    };
    // 큐에서 메시지를 받아서 처리하는 함수를 등록함
    ch.consume('user.created', handleUserCreated);
    ch.consume('user.deleted', handleUserDeleted);
  });
});

// 서버 시작
app.listen(3001, () => console.log('Server listening on port 3001'));
// routes/post.js

// 모듈 임포트
const express = require('express');
const Post = require('../models/post');

// 라우터 생성
const router = express.Router();

// 게시글 생성 라우트
router.post('/', async (req, res, next) => {
  try {
    // 요청 바디에서 제목과 내용을 가져옴
    const { title, content } = req.body;
    // 새로운 게시글을 생성함
    const post = new Post({
      title,
      content,
    });
    // 데이터베이스에 저장함
    await post.save();
    // 응답으로 게시글 정보를 보냄
    res.status(201).json(post);
  } catch (err) {
    next(err);
  }
});

// 게시글 조회 라우트
router.get('/', async (req, res, next) => {
  try {
    // 데이터베이스에서 모든 게시글을 가져옴
    const posts = await Post.find();
    // 응답으로 게시글 목록을 보냄
    res.status(200).json(posts);
  } catch (err) {
    next(err);
  }
});

// 게시글 수정 라우트
router.put('/:id', async (req, res, next) => {
  try {
    // 요청 파라미터에서 게시글 아이디를 가져옴
    const { id } = req.params;
    // 요청 바디에서 제목과 내용을 가져옴
    const { title, content } = req.body;
    // 데이터베이스에서 게시글을 찾음
    const post = await Post.findById(id);
    if (!post) {
      // 게시글이 없으면 에러 발생시킴
      const error = new Error('Post not found');
      error.status = 404;
      throw error;
    }
    // 게시글의 제목과 내용을 업데이트함
    post.title = title;
    post.content = content;
    // 데이터베이스에 저장함
    await post.save();
    // 응답으로 업데이트된 게시글 정보를 보냄
    res.status(200).json(post);
  } catch (err) {
    next(err);
  }
});

// 게시글 삭제 라우트
router.delete('/:id', async (req, res, next) => {
  try {
    // 요청 파라미터에서 게시글 아이디를 가져옴
    const { id } = req.params;
    // 데이터베이스에서 게시글을 찾음
    const post = await Post.findById(id);
    if (!post) {
      // 게시글이 없으면 에러 발생시킴
      const error = new Error('Post not found');
      error.status = 404;
      throw error;
    }
    // 데이터베이스에서 게시글을 삭제함
    await post.remove();
    // 응답으로 삭제된 게시글 정보를 보냄
    res.status(200).json(post);
  } catch (err) {
    next(err);
  }
});

module.exports = router;

 

이렇게 게시글 관리 서비스를 만들었습니다. 이제 두 개의 서비스를 실행하고 테스트해보겠습니다. 다음은 각 서비스의 실행 방법입니다.

사용자 인증 서비스

1. 터미널에서 cd auth 명령어로 디렉토리 이동

2. npm install 명령어로 필요한 모듈 설치

3. node app.js 명령어로 서비스 실행

 

게시글 관리 서비스

1. 터미널에서 cd post 명령어로 디렉토리 이동

2. npm install 명령어로 필요한 모듈 설치

3. node app.js 명령어로 서비스 실행

서비스를 실행한 후, Postman이나 curl 같은 도구를 이용하여 각 서비스의 API를 호출해보세요. 예를 들어, 다음은 curl 명령어로 사용자 인증 서비스의 API를 호출하는 방법입니다.

 

사용자 등록 API 호출

curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"email":"test@test.com", "password":"test123"}'

 

로그인 API 호출

curl -X POST http://localhost:3000/auth/login -H "Content-Type: application/json" -d '{"email":"test@test.com", "password":"test123"}'

토큰 검증 API 호출 (토큰은 로그인 API 응답에서 복사) 

curl -X GET http://localhost:3000/auth/verify -H "Authorization: Bearer "

이상으로 Node.js와 마이크로서비스 아키텍처에 대한 간단한 소개와 예제를 마치겠습니다. 마이크로서비스 아키텍처는 많은 장점을 가지고 있지만, 도입하기 전에 잘 고민하고 설계해야 합니다. Node.js는 마이크로서비스 아키텍처를 구현하기에 적합한 기술 중 하나입니다. Node.js를 이용하여 여러분만의 마이크로서비스 아키텍처를 만들어보세요. 감사합니다.