마이크로 서비스 - (1) Mini 마이크로서비스 구축

임쿠쿠·2022년 5월 29일
0

Microservice

목록 보기
1/3
post-thumbnail

1. 마이크로 서비스 Data 관리

1) 각각의 서비스는 자신의 database를 소유(필요 시)
2) 서비스는 다른 서비스의 데이터베이스에 접근 불가

  • 서비스가 독립적으로 구성될 수 있고, 스케일링에 대한 복잡도 감소
  • 타 서비스의 데이터베이스를 사용할 경우, 해당 데이터베이스의 스키마 변경에 따른 예기치 못한 문제 발생
  • 각 서비스 별로 최적의 DB를 사용 가능(sql vs nosql)

3) 마이크로 서비스 data 관리 시 문제점

  • 각 서비스는 타 서비스의 DB접근 불가에 따라 Service D는 product / order / user DB 접근 불가

2. 마이크로 서비스간의 커뮤니케이션

1) Sync Communication(서비스간의 직접적으로 통신)


장점

  • 구축하기 쉽고, Service D는 DB 컬렉션 생성 불필요

단점

  • 서비스 A,B,C 중 하나에 문제가 생길 경우 D도 문제 발생(서비스간 의존성 발생)
  • 특정 서비스에 대한 요청의 시간이 길면 동일하게 속도 이슈 발생

2) Async Communication(이벤트 기반으로 서비스간의 통신)

  • Service A,B,C는 각각 유저, 제품, 주문에 대한 DB기록과 동시에 해당 정보를 이벤트 버스로 전달
  • Service D는 user Id, product 정보, order 정보(서비스 A,B,C가 emit한 이벤트 정보)등 필요한 정보를 이벤트 버스에서 가져와 기록한다.
  • Service D는 이제 각 서비스에 요청하지 않고도 받아온 Event 정보를 바탕으로 특정 유저의 물품 오더정보를 즉각 보여 줄 수 있다.

장점

  • 특정 서비스에 대한 직접적인 종속성을 제거할 수 있다.(타 서비스 다운 시, 문제 발생은 동일함)
  • Sync 커뮤니케이션에 비해 빠른 응답이 가능하다.

단점

  • Service D에도 필요한 정보를 이벤트 버스에서 받아와 기록하므로 타 서비스가 가지고 있는 데이터와 일부 중복 발생
  • 운영적 오버헤드와 추가해야할 코드작업 증가

3. NodeJS로 mini 마이크로서비스 구축

1) Comment & Post Service 구축

// POST Service
const express = require('express');
const bodyParser = require('body-parser');
const { randomBytes } = require('crypto');
const cors = require('cors');

const app = express();
app.use(bodyParser.json());
app.use(cors());

const posts = {};

app.get('/posts', (req, res) => {
  res.send(posts);
});

app.post('/posts', (req, res) => {
  const id = randomBytes(4).toString('hex');
  const { title } = req.body;

  posts[id] = {
    id,
    title
  };

  res.status(201).send(posts[id]);
});

app.listen(4000, () => {
  console.log('Listening on 4000');
});
// Comment Service
const express = require('express');
const bodyParser = require('body-parser');
const { randomBytes } = require('crypto');
const cors = require('cors');

const app = express();
app.use(bodyParser.json());
app.use(cors());

const commentsByPostId = {};

app.get('/posts/:id/comments', (req, res) => {
  res.send(commentsByPostId[req.params.id] || []);
});

app.post('/posts/:id/comments', (req, res) => {
  const commentId = randomBytes(4).toString('hex');
  const { content } = req.body;

  const comments = commentsByPostId[req.params.id] || [];

  comments.push({ id: commentId, content });

  commentsByPostId[req.params.id] = comments;

  res.status(201).send(comments);
});

app.listen(4001, () => {
  console.log('Listening on 4001');
});

생각해보기

  • 모놀로직 서비스의 경우 각 서비스가 하나의 DB를 사용하므로 JOIN을 통해 각 post에 해당하는 comment를 쉽게 가져올 수 있다.

  • 마이크로 서비스는 각 DB에 대한 접근을 허용하지 않으므로, 클라이언트가 각 post에 대한 comment를 얻기 위해서는 post 수 만큼 post id를 보내고 comment service에서 이를 처리 해야한다.

2) Query Service & Event Bus구축

  • 위의 comment를 찾기 위한 다수의 요청을 축소하기 위한 방법은 여러가지 있다.

(1) Sync Communication

  • 클라이언트에서 post를 가져올 때, post 서비스에서 직접적으로 comment service에 접근하여 comment 정보를 받아온 후 응답한다.

문제점

  • 구현하기에는 편하지만, 두 서비스간의 의존성이 커진다.
  • 한 서비스가 다운 시, 전체 서비스에 영향을 미친다.
  • 한 서비스의 속도가 느려질 시, 전체 서비스의 속도에 영향을 미친다.

(2) ASync Communication

  • Post 생성 시, PostCreated 이벤트를 Event Bus에 전달
  • Comment 생성 시, CommentCreated 이벤트를 Event Bus에 전달
  • Query 서비스는 PostCreated & CommentCreated 이벤트를 받아와 조합 후 클라이언트에게 전달

// POST SERVICE
const express = require("express");
const bodyParser = require("body-parser");
const { randomBytes } = require("crypto");
const cors = require("cors");
const axios = require("axios");

const app = express();
app.use(bodyParser.json());
app.use(cors());

const posts = {};

app.get("/posts", (req, res) => {
  res.send(posts);
});

app.post("/posts", async (req, res) => {
  const id = randomBytes(4).toString("hex");
  const { title } = req.body;

  posts[id] = {
    id,
    title,
  };

  // POST 생성 후, 이벤트 버스에 이벤트 전달
  await axios.post("http://localhost:4005/events", {
    type: "PostCreated",
    data: {
      id,
      title,
    },
  });

  res.status(201).send(posts[id]);
});

app.post("/events", (req, res) => {
  console.log("Received Event", req.body.type);

  res.send({});
});

app.listen(4000, () => {
  console.log("Listening on 4000");
});
// Comment Service
const express = require("express");
const bodyParser = require("body-parser");
const { randomBytes } = require("crypto");
const cors = require("cors");
const axios = require("axios");

const app = express();
app.use(bodyParser.json());
app.use(cors());

const commentsByPostId = {};

app.get("/posts/:id/comments", (req, res) => {
  res.send(commentsByPostId[req.params.id] || []);
});

app.post("/posts/:id/comments", async (req, res) => {
  const commentId = randomBytes(4).toString("hex");
  const { content } = req.body;

  const comments = commentsByPostId[req.params.id] || [];

  comments.push({ id: commentId, content });

  commentsByPostId[req.params.id] = comments;

  // Comment 생성 후, 이벤트 버스에 이벤트 전달
  await axios.post("http://localhost:4005/events", {
    type: "CommentCreated",
    data: {
      id: commentId,
      content,
      postId: req.params.id,
    },
  });

  res.status(201).send(comments);
});

app.post("/events", (req, res) => {
  console.log("Event Received", req.body.type);

  res.send({});
});

app.listen(4001, () => {
  console.log("Listening on 4001");
});
// Event Bus
const express = require("express");
const bodyParser = require("body-parser");
const axios = require("axios");

const app = express();
app.use(bodyParser.json());

// Event 수신 후, 각 Service에 이벤트 전달
app.post("/events", (req, res) => {
  const event = req.body;

  axios.post("http://localhost:4000/events", event).catch((err) => {
    console.log(err.message);
  });
  axios.post("http://localhost:4001/events", event).catch((err) => {
    console.log(err.message);
  });
  axios.post("http://localhost:4002/events", event).catch((err) => {
    console.log(err.message);
  });
  res.send({ status: "OK" });
});

app.listen(4005, () => {
  console.log("Listening on 4005");
});
// Query Service
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
app.use(bodyParser.json());
app.use(cors());

const posts = {};

app.get('/posts', (req, res) => {
  res.send(posts);
});

app.post('/events', (req, res) => {
  const { type, data } = req.body;

  // Event 타입에 따라 Post 생성 & Comment 생성 후 클라이언트에게 조합된 정보 전달
  if (type === 'PostCreated') {
    const { id, title } = data;

    posts[id] = { id, title, comments: [] };
  }

  if (type === 'CommentCreated') {
    const { id, content, postId } = data;

    const post = posts[postId];
    post.comments.push({ id, content });
  }

  console.log(posts);

  res.send({});
});

app.listen(4002, () => {
  console.log('Listening on 4002');
});

3) Comment 중재 서비스 및 동기화 구축

(1) Comment 중재 서비스

Comment의 유형에 따라 승인 & 거부 로직을 구축 시, Moderate 서비스는 Comment content 유형에 따른 비즈니스 로직을 실행하고 업데이트된 comment를 이벤트 스토어에 이벤트에 전달 한 후 Comment & Query 서비스에서 이를 수신하여 처리할 수 있다.

하지만, 위와 같이 중재 서비스가 제공하는 타입이 많아질 수록 이벤트 수신 서비스(Comment & Query)는 위 타입에 따른 모든 로직을 구성 해야한다.

이를 해결하기 위해,

  • 중재 서비스의 이벤트를 Comment 서비스가 수신 후 중재 서비스 타입에 따른 로직을 처리
  • 업데이트된 Comment 내역을 CommentUpdated 이벤트로 방출
  • Query 서비스에서 CommentUpdated 수신하면 변화된 comment를 클라이언트에게 전달

결론 : 중재서비스의 이벤트 타입이 많아져도 Comment에서만 이를 처리하고 Query 서비스는 CommentUpdated 이벤트의 업데이트 된 Comment 내역만 확인하면 된다.

(2) 동기화

클라이언트에게 Post & Comment 정보를 전달하는 Query 서비스가 일시적으로 다운 시, 그동안 요청한 고객의 Post & Comment 정보를 받아오기 위해서 Event Bus 내 Event Bus Store를 만들고, 재가동될때 해당 Event Bus Store에서 그동안의 Post & Comment 요청을 받아 올 수 있다.

// Post Service
const express = require('express');
const bodyParser = require('body-parser');
const { randomBytes } = require('crypto');
const cors = require('cors');
const axios = require('axios');

const app = express();
app.use(bodyParser.json());
app.use(cors());

const posts = {};

app.get('/posts', (req, res) => {
  res.send(posts);
});

app.post('/posts', async(req, res) => {
  const id = randomBytes(4).toString('hex');
  const { title } = req.body;

  posts[id] = {
    id,
    title
  };

  await axios.post('http://localhost:4005/events', {
    type: 'PostCreated',
    data: {
    id, title
    }
  });

  res.status(201).send(posts[id]);
});

app.post('/events', (req, res) => {
  console.log('포스트 서비스 이벤트 수신', req.body.type);

  res.send({});
});

app.listen(4000, () => {
  console.log('Listening on 4000');
});
// Comment Service
const express = require('express');
const bodyParser = require('body-parser');
const { randomBytes } = require('crypto');
const cors = require('cors');
const axios = require('axios');

const app = express();
app.use(bodyParser.json());
app.use(cors());

const commentsByPostId = {};

app.get('/posts/:id/comments', (req, res) => {
  res.send(commentsByPostId[req.params.id] || []);
});

app.post('/posts/:id/comments', async(req, res) => {
  const commentId = randomBytes(4).toString('hex');
  const { content } = req.body;

  const comments = commentsByPostId[req.params.id] || [];

  comments.push({ id: commentId, content, status: 'pending' });

  commentsByPostId[req.params.id] = comments;

  await axios.post('http://localhost:4005/events', {
    type: 'CommentCreated',
    data: {
     id: commentId,
     content,
     postId: req.params.id,
     status: 'pending'
    }
  });

  res.status(201).send(comments);
});

// CommentModerated 수신 후 이벤트 버스에 CommentUpdated 전송
app.post('/events', async(req, res) => {
  console.log('코멘트 서비스 이벤트 수신', req.body.type);

  const { type, data } = req.body;

  if (type === 'CommentModerated') {
    const { id, status, postId, content } = data;
    
    const comments = commentsByPostId[postId];

    const comment = comments.find(comment => {
      return comment.id === id;
    });

    comment.status = status;

    await axios.post('http://localhost:4005/events', {
      type: 'CommentUpdated',
      data: {
       id,
       postId,
       status,
       content
      }
    });
  }

  res.send({});
});

app.listen(4001, () => {
  console.log('Listening on 4001');
});
// Moderation Service
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');

const app = express();
app.use(bodyParser.json());

app.post('/events', async(req, res) => {
    const { type, data } = req.body;

    if (type === 'CommentCreated') {
        const status = data.content.includes('orange') ? 'rejected' : 'approved';
        
        // CommentCreated 받고 조정 후, CommentModerated를
        // 이벤트 버스에 재전송
        await axios.post('http://localhost:4005/events', {
            type: 'CommentModerated',
            data: {
                id: data.id,
                postId: data.postId,
                status,
                content: data.content
            }
        })
    }

    res.send({});
});

app.listen(4003, () => {
    console.log('Listening on 4003');
});
// Event-Bus
const express = require("express");
const bodyParser = require("body-parser");
const axios = require("axios");

const app = express();
app.use(bodyParser.json());


// 이벤트 버스 스토어 생성
const events = [];

app.post("/events", (req, res) => {
  const event = req.body;

  events.push(event);

  axios.post("http://localhost:4000/events", event).catch((err) => {
    console.log(err.message);
  });
  axios.post("http://localhost:4001/events", event).catch((err) => {
    console.log(err.message);
  });
  axios.post("http://localhost:4002/events", event).catch((err) => {
    console.log(err.message);
  });
  axios.post("http://localhost:4003/events", event).catch((err) => {
    console.log(err.message);
  });
  res.send({ status: "OK" });
});

app.get('/events', (req, res) => {
  res.send(events);
});

app.listen(4005, () => {
  console.log("Listening on 4005");
});
// Query Service
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const axios = require('axios');

const app = express();
app.use(bodyParser.json());
app.use(cors());

const posts = {};

const handleEvent = (type, data) => {
    if(type === 'PostCreated') {
        const { id, title } = data;

        posts[id] = { id, title, comments: [] };
    }

    if(type === 'CommentCreated') {
        const { id, content, postId, status } = data;

        const post = posts[postId];
       
        post.comments.push({ id, content, status });
    }

    // 코멘트 상태 업데이트 수신 후 상태 업데이트 진행
    if(type === 'CommentUpdated') {
        const { id, postId, status, content } = data;
        
        const post = posts[postId];
        // console.log('post---', post)
        const comment = post.comments.find(comment => {
            return comment.id === id;
        });

        comment.status = status;
        comment.content = content;
    }
}

app.get('/posts', (req, res) => {
    res.send(posts);
});

app.post('/events', (req, res) => {
    const { type, data } = req.body;
    
    handleEvent(type, data);
    
    res.send({});
});


// 서비스 다운 후 재실행 시 event 받아오기
app.listen(4002, async () => {
    console.log("Listening on 4002");
    try {
      const res = await axios.get("http://localhost:4005/events");
   
      for (let event of res.data) {
        console.log("Processing event:", event.type);
   
        handleEvent(event.type, event.data);
      }
    } catch (error) {
      console.log(error.message);
    }
  });
  • 위 Mini 마이크로서비스는 중복된 로직 & 데이터가 혼재한다. 하지만, 기본적으로 마이크로서비스가 어떠한 방식으로 async 커뮤니케이션을 진행하는지 확인할 수 있다.

참고 : Microservices with Node JS(Stephen Grider)

profile
Pay it forward

0개의 댓글