Node.js 인터뷰 질문 70

질문: Node.js를 사용한 서버리스 아키텍처의 구현 방법과 장단점에 대해 설명해주세요.

답변:

서버리스 아키텍처는 애플리케이션 개발 및 배포 방식을 혁신적으로 변화시킨 패러다임으로, Node.js는 그 가벼운 특성과 빠른 시작 시간으로 인해 서버리스 환경에 매우 적합합니다. 서버리스의 핵심은 서버 관리가 필요 없이 함수 단위로 코드를 실행할 수 있다는 점입니다.

1. 서버리스 아키텍처의 기본 개념

서버리스는 서버가 없다는 의미가 아니라, 개발자가 서버 프로비저닝 및 관리에 신경 쓸 필요가 없다는 의미입니다. 주요 특징은 다음과 같습니다:

  • 이벤트 기반 실행
  • 자동 확장성
  • 사용한 만큼만 비용 지불
  • 짧은 실행 시간 (일반적으로 몇 초~몇 분)
  • 상태 비저장 함수

2. AWS Lambda를 사용한 Node.js 서버리스 구현

AWS Lambda는 가장 인기 있는 서버리스 플랫폼 중 하나입니다.

2.1 기본 Lambda 함수

// 간단한 Lambda 함수
exports.handler = async (event) => {
  console.log("이벤트 데이터:", JSON.stringify(event, null, 2));

  try {
    // 요청 파라미터 파싱
    const name = event.queryStringParameters?.name || "World";

    // 응답 구성
    const response = {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        message: `Hello, ${name}!`,
        timestamp: new Date().toISOString(),
      }),
    };

    return response;
  } catch (error) {
    console.error("오류 발생:", error);

    return {
      statusCode: 500,
      body: JSON.stringify({
        error: "Internal Server Error",
        message: error.message,
      }),
    };
  }
};

2.2 Express.js와 Lambda 통합 (AWS Lambda Adapter)

// Express 애플리케이션을 Lambda로 래핑
const express = require("express");
const serverless = require("serverless-http");

// Express 앱 초기화
const app = express();

// 미들웨어 설정
app.use(express.json());

// 라우트 정의
app.get("/hello", (req, res) => {
  const name = req.query.name || "World";
  res.json({ message: `Hello, ${name}!` });
});

app.post("/users", async (req, res) => {
  try {
    // 사용자 생성 로직
    const user = {
      id: Date.now().toString(),
      name: req.body.name,
      email: req.body.email,
      createdAt: new Date().toISOString(),
    };

    // 응답
    res.status(201).json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Express 앱을 Lambda 핸들러로 변환
exports.handler = serverless(app);

3. Azure Functions를 사용한 Node.js 서버리스 구현

// Azure Functions HTTP 트리거 예시
module.exports = async function (context, req) {
  context.log("HTTP 요청 처리 시작");

  const name = req.query.name || (req.body && req.body.name) || "World";

  // 응답 구성
  context.res = {
    status: 200,
    body: {
      message: `Hello, ${name}!`,
      timestamp: new Date().toISOString(),
    },
    headers: {
      "Content-Type": "application/json",
    },
  };
};

4. Google Cloud Functions을 사용한 Node.js 서버리스 구현

// Google Cloud Function HTTP 트리거 예시
exports.helloWorld = (req, res) => {
  const name = req.query.name || req.body.name || "World";

  res.status(200).send({
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString(),
  });
};

5. Serverless Framework 활용

Serverless Framework는 여러 클라우드 제공업체에서 서버리스 애플리케이션을 쉽게 배포할 수 있게 해주는 도구입니다.

5.1 Serverless 구성 파일

# serverless.yml
service: my-node-service

provider:
  name: aws
  runtime: nodejs14.x
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}
  environment:
    NODE_ENV: ${self:provider.stage}
    DB_CONNECTION_STRING: ${ssm:/my-app/${self:provider.stage}/db-connection}

functions:
  api:
    handler: src/handlers/api.handler
    events:
      - http:
          path: /
          method: any
      - http:
          path: /{proxy+}
          method: any
    environment:
      SPECIFIC_VAR: value

  processQueue:
    handler: src/handlers/queue.handler
    events:
      - sqs:
          arn: !GetAtt MyQueue.Arn
          batchSize: 10

resources:
  Resources:
    MyQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-${self:provider.stage}-queue

5.2 서버리스 배포 명령어

# 서비스 배포
serverless deploy

# 특정 함수만 배포
serverless deploy function -f api

# 로컬에서 함수 호출
serverless invoke local -f api -p event.json

# 로그 확인
serverless logs -f api

6. 서버리스 데이터베이스 통합

6.1 DynamoDB 통합

// AWS SDK를 사용하여 DynamoDB 통합
const AWS = require("aws-sdk");
const dynamoDB = new AWS.DynamoDB.DocumentClient();

// 항목 생성
async function createItem(data) {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: data.id || Date.now().toString(),
      content: data.content,
      createdAt: new Date().toISOString(),
      // 기타 속성
    },
  };

  await dynamoDB.put(params).promise();
  return params.Item;
}

// 항목 조회
async function getItem(id) {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: { id },
  };

  const result = await dynamoDB.get(params).promise();
  return result.Item;
}

// Lambda 핸들러에서 사용
exports.handler = async (event) => {
  if (event.httpMethod === "POST") {
    const body = JSON.parse(event.body);
    const item = await createItem(body);

    return {
      statusCode: 201,
      body: JSON.stringify(item),
    };
  } else if (event.httpMethod === "GET") {
    const id = event.pathParameters.id;
    const item = await getItem(id);

    if (!item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ error: "Item not found" }),
      };
    }

    return {
      statusCode: 200,
      body: JSON.stringify(item),
    };
  }

  return {
    statusCode: 400,
    body: JSON.stringify({ error: "Invalid request" }),
  };
};

6.2 MongoDB Atlas 통합

// MongoDB Atlas와 서버리스 통합
const MongoClient = require("mongodb").MongoClient;

let cachedDb = null;

async function connectToDatabase() {
  if (cachedDb) {
    return cachedDb;
  }

  // 연결 문자열은 환경 변수에서 가져옴
  const client = await MongoClient.connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });

  const db = client.db(process.env.MONGODB_DB_NAME);
  cachedDb = db;
  return db;
}

// Lambda 핸들러에서 사용
exports.handler = async (event) => {
  try {
    const db = await connectToDatabase();
    const collection = db.collection("items");

    if (event.httpMethod === "GET") {
      const items = await collection.find({}).limit(20).toArray();

      return {
        statusCode: 200,
        body: JSON.stringify(items),
      };
    } else if (event.httpMethod === "POST") {
      const body = JSON.parse(event.body);
      const result = await collection.insertOne({
        ...body,
        createdAt: new Date(),
      });

      return {
        statusCode: 201,
        body: JSON.stringify({
          id: result.insertedId,
          ...body,
        }),
      };
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message }),
    };
  }
};

7. 서버리스 웹 API 아키텍처

7.1 API Gateway 통합

// API Gateway와 Lambda 통합
exports.handler = async (event) => {
  // API Gateway 프록시 통합에서 이벤트 구조
  console.log("경로 파라미터:", event.pathParameters);
  console.log("쿼리 파라미터:", event.queryStringParameters);
  console.log("요청 본문:", event.body);
  console.log("HTTP 메소드:", event.httpMethod);

  // API 라우팅 처리
  const route = `${event.httpMethod} ${event.resource}`;

  switch (route) {
    case "GET /items":
      return await getAllItems();

    case "GET /items/{id}":
      return await getItemById(event.pathParameters.id);

    case "POST /items":
      return await createItem(JSON.parse(event.body));

    case "PUT /items/{id}":
      return await updateItem(event.pathParameters.id, JSON.parse(event.body));

    case "DELETE /items/{id}":
      return await deleteItem(event.pathParameters.id);

    default:
      return {
        statusCode: 404,
        body: JSON.stringify({ error: "Route not found" }),
      };
  }
};

// API 구현 함수들...
async function getAllItems() {
  // 구현...
}

async function getItemById(id) {
  // 구현...
}

// 기타 함수들...

7.2 GraphQL API 구현

// Apollo Server Lambda를 사용한 GraphQL API
const { ApolloServer, gql } = require("apollo-server-lambda");

// 스키마 정의
const typeDefs = gql`
  type Item {
    id: ID!
    name: String!
    description: String
    createdAt: String!
  }

  type Query {
    getItems: [Item]
    getItem(id: ID!): Item
  }

  type Mutation {
    createItem(name: String!, description: String): Item
    updateItem(id: ID!, name: String, description: String): Item
    deleteItem(id: ID!): Boolean
  }
`;

// 리졸버 함수
const resolvers = {
  Query: {
    getItems: async () => {
      // 모든 항목 조회 로직
    },
    getItem: async (_, { id }) => {
      // ID로 항목 조회 로직
    },
  },
  Mutation: {
    createItem: async (_, { name, description }) => {
      // 항목 생성 로직
    },
    updateItem: async (_, { id, name, description }) => {
      // 항목 업데이트 로직
    },
    deleteItem: async (_, { id }) => {
      // 항목 삭제 로직
    },
  },
};

// Apollo 서버 생성
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ event, context }) => ({
    headers: event.headers,
    functionName: context.functionName,
    event,
    context,
  }),
});

// Lambda 핸들러 생성
exports.handler = server.createHandler({
  cors: {
    origin: "*",
    credentials: true,
  },
});

8. 서버리스 아키텍처의 장점

  1. 운영 비용 절감: 사용한 컴퓨팅 리소스에 대해서만 비용을 지불합니다.

  2. 자동 확장성: 트래픽 증가에 따라 자동으로 확장됩니다.

  3. 빠른 배포: 개발에서 프로덕션까지의 시간이 단축됩니다.

  4. 관리 오버헤드 감소: 서버 관리에 신경 쓸 필요가 없어 개발에 집중할 수 있습니다.

  5. 내장된 고가용성: 대부분의 서버리스 플랫폼은 기본적으로 고가용성을 제공합니다.

9. 서버리스 아키텍처의 단점

  1. 콜드 스타트: 함수가 일정 시간 호출되지 않으면 다시 시작할 때 지연이 발생합니다.
// 콜드 스타트 최소화를 위한 전략
// 1. 패키지 크기 최소화
const AWS = require("aws-sdk");
// 대신 특정 서비스만 임포트
const { DynamoDB } = require("aws-sdk");

// 2. 초기화 코드와 핸들러 분리
// DB 연결 등 무거운 초기화는 함수 외부에서 수행
const db = connectToDatabase(); // 함수 외부에서 실행

exports.handler = async (event) => {
  // 핸들러 내에서는 이미 초기화된 리소스 사용
  const result = await db.query(/* ... */);
  return { statusCode: 200, body: JSON.stringify(result) };
};
  1. 실행 시간 제한: 대부분의 플랫폼은 함수 실행 시간에 제한이 있습니다.

  2. 상태 비저장: 함수 간 상태 공유가 어렵습니다.

  3. 디버깅 및 테스트의 복잡성: 클라우드 환경에서의 디버깅이 더 어려울 수 있습니다.

  4. 비용 예측 어려움: 사용량이 많아지면 경우에 따라 기존 서버 기반 모델보다 비용이 증가할 수 있습니다.

10. 서버리스 아키텍처 모범 사례

10.1 함수 설계 원칙

// 좋은 예: 단일 책임 원칙을 따르는 함수
exports.processPayment = async (event) => {
  // 결제 처리 로직만 포함
};

exports.sendOrderConfirmation = async (event) => {
  // 주문 확인 이메일 발송 로직만 포함
};

// 나쁜 예: 너무 많은 책임을 가진 함수
exports.handleOrder = async (event) => {
  // 주문 검증, 결제 처리, 재고 업데이트, 이메일 발송 등
  // 모든 작업을 한 함수에서 처리
};

10.2 환경 변수 활용

// 설정 값은 환경 변수로 관리
const config = {
  region: process.env.AWS_REGION,
  tableName: process.env.DYNAMODB_TABLE,
  apiKey: process.env.API_KEY,
  stage: process.env.STAGE || "dev",
};

// 환경별 동작 변경
if (config.stage === "prod") {
  // 프로덕션 환경 특화 설정
} else {
  // 개발/테스트 환경 설정
}

10.3 비동기 작업 최적화

// 병렬 처리를 통한 최적화
async function processItems(items) {
  // Promise.all을 사용한 병렬 처리
  const results = await Promise.all(items.map((item) => processItem(item)));

  return results;
}

// AWS SDK에서 커넥션 재사용
const documentClient = new AWS.DynamoDB.DocumentClient({
  httpOptions: {
    agent: new https.Agent({
      keepAlive: true,
      maxSockets: 50,
      rejectUnauthorized: true,
    }),
  },
});

요약

Node.js를 사용한 서버리스 아키텍처는 확장성, 비용 효율성, 개발 속도 면에서 많은 이점을 제공합니다. AWS Lambda, Azure Functions, Google Cloud Functions와 같은 플랫폼에서 Node.js 함수를 손쉽게 배포하고 실행할 수 있으며, Serverless Framework를 통해 배포 프로세스를 자동화할 수 있습니다.

서버리스는 마이크로서비스, 이벤트 기반 처리, 웹 API 등 다양한 용도에 적합하지만, 콜드 스타트, 실행 시간 제한, 상태 관리의 어려움 등의 단점도 있습니다. 이러한 제약사항을 이해하고 적절한 설계 원칙을 적용하면 효율적인 서버리스 애플리케이션을 구축할 수 있습니다.

서버리스는 모든 상황에 적합한 만능 솔루션이 아니므로, 애플리케이션의 요구사항과 특성을 고려하여 기존 서버 기반 아키텍처와 서버리스 아키텍처 중 적절한 것을 선택하는 것이 중요합니다.

results matching ""

    No results matching ""