Node.js 인터뷰 질문 55

질문: Node.js에서 서버리스 아키텍처를 구현하는 방법과 그 장단점은 무엇인가요?

답변:

서버리스 아키텍처는 개발자가 서버 인프라를 관리하지 않고 코드를 실행할 수 있는 클라우드 컴퓨팅 실행 모델입니다. Node.js는 가볍고 이벤트 기반 특성 덕분에 서버리스 환경에 매우 적합합니다. 여기서는 Node.js로 서버리스 아키텍처를 구현하는 방법과 그 장단점에 대해 알아보겠습니다.

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

서버리스 아키텍처의 핵심 구성요소:

  1. 함수 (Functions): 특정 작업을 수행하는 코드 조각
  2. 이벤트 (Events): 함수 실행을 트리거하는 작업 또는 상태 변경
  3. 서비스 (Services): 함수가 활용하는 관리형 백엔드 서비스 (데이터베이스, 인증 등)

2. 주요 서버리스 플랫폼

Node.js를 지원하는 주요 서버리스 플랫폼:

  1. AWS Lambda
  2. Azure Functions
  3. Google Cloud Functions
  4. Vercel (구 Zeit Now)
  5. Netlify Functions

3. AWS Lambda로 Node.js 서버리스 함수 구현하기

3.1 기본 Lambda 함수

exports.handler = async (event, context) => {
  try {
    // 이벤트 데이터 처리
    const name = event.queryStringParameters?.name || "World";

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

    return response;
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "내부 서버 오류" }),
    };
  }
};

3.2 AWS SDK 활용 예시 (DynamoDB 데이터 조회)

const AWS = require("aws-sdk");
const dynamoDB = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event) => {
  const userId = event.pathParameters?.userId;

  if (!userId) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: "사용자 ID가 필요합니다" }),
    };
  }

  const params = {
    TableName: "Users",
    Key: {
      userId: userId,
    },
  };

  try {
    const result = await dynamoDB.get(params).promise();

    if (!result.Item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ error: "사용자를 찾을 수 없습니다" }),
      };
    }

    return {
      statusCode: 200,
      body: JSON.stringify(result.Item),
    };
  } catch (error) {
    console.error("DynamoDB 오류:", error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "내부 서버 오류" }),
    };
  }
};

4. 서버리스 프레임워크 사용하기

Serverless Framework은 서버리스 애플리케이션 배포와 관리를 단순화합니다.

4.1 설치 및 시작하기

# 전역으로 Serverless 프레임워크 설치
npm install -g serverless

# 새 프로젝트 생성
serverless create --template aws-nodejs --path my-service

# 프로젝트 디렉토리로 이동
cd my-service

4.2 serverless.yml 구성 예시

service: my-service

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-2
  environment:
    DB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DB_TABLE}"

functions:
  getUser:
    handler: handlers/users.getUser
    events:
      - http:
          path: users/{userId}
          method: get
          cors: true

  createUser:
    handler: handlers/users.createUser
    events:
      - http:
          path: users
          method: post
          cors: true

resources:
  Resources:
    UsersTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: ${self:provider.environment.DB_TABLE}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH

4.3 핸들러 함수 구현 (handlers/users.js)

const AWS = require("aws-sdk");
const { v4: uuidv4 } = require("uuid");
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.DB_TABLE;

// 사용자 조회
module.exports.getUser = async (event) => {
  const userId = event.pathParameters.userId;

  try {
    const result = await dynamoDB
      .get({
        TableName: tableName,
        Key: { userId },
      })
      .promise();

    if (!result.Item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ error: "사용자를 찾을 수 없습니다" }),
      };
    }

    return {
      statusCode: 200,
      body: JSON.stringify(result.Item),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "내부 서버 오류" }),
    };
  }
};

// 사용자 생성
module.exports.createUser = async (event) => {
  try {
    const data = JSON.parse(event.body);

    if (!data.name || !data.email) {
      return {
        statusCode: 400,
        body: JSON.stringify({ error: "이름과 이메일이 필요합니다" }),
      };
    }

    const timestamp = new Date().getTime();
    const userId = uuidv4();

    const newUser = {
      userId,
      name: data.name,
      email: data.email,
      createdAt: timestamp,
      updatedAt: timestamp,
    };

    await dynamoDB
      .put({
        TableName: tableName,
        Item: newUser,
      })
      .promise();

    return {
      statusCode: 201,
      body: JSON.stringify(newUser),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "내부 서버 오류" }),
    };
  }
};

4.4 배포하기

# 배포
serverless deploy

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

5. Express.js 애플리케이션을 서버리스로 변환하기

AWS Lambda와 API Gateway를 사용하여 Express.js 애플리케이션을 서버리스로 변환할 수 있습니다.

5.1 Serverless Express 설치

npm install serverless-http

5.2 Express 앱 래핑하기

// app.js
const express = require("express");
const serverless = require("serverless-http");
const app = express();

app.use(express.json());

// 라우트 정의
app.get("/", (req, res) => {
  res.json({ message: "Express 서버리스 애플리케이션" });
});

app.get("/users/:userId", (req, res) => {
  res.json({ userId: req.params.userId, name: "홍길동" });
});

app.post("/users", (req, res) => {
  res.status(201).json({
    message: "사용자가 생성되었습니다",
    user: req.body,
  });
});

// 일반 Node.js 서버로 실행 (로컬 개발용)
if (process.env.IS_LOCAL) {
  const port = process.env.PORT || 3000;
  app.listen(port, () => {
    console.log(`서버가 http://localhost:${port}에서 실행 중입니다`);
  });
}

// Lambda 함수로 내보내기
module.exports.handler = serverless(app);

5.3 serverless.yml 구성

service: express-serverless

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-2

functions:
  app:
    handler: app.handler
    events:
      - http:
          path: /
          method: ANY
          cors: true
      - http:
          path: /{proxy+}
          method: ANY
          cors: true

plugins:
  - serverless-offline

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

  1. 인프라 관리 불필요: 서버 프로비저닝, 스케일링, 패치 등의 작업 없음
  2. 자동 확장성: 요청 수에 따라 자동으로 확장 및 축소
  3. 비용 효율성: 사용한 컴퓨팅 시간에 대해서만 비용 지불
  4. 빠른 배포: 인프라 변경 없이 코드만 업데이트 가능
  5. 높은 가용성: 대부분의 서버리스 플랫폼은 기본적으로 높은 가용성 제공
  6. 세분화된 아키텍처: 작은 단위의 함수로 분리하여 유지보수 용이

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

  1. 콜드 스타트: 함수가 일정 시간 호출되지 않으면 다시 시작할 때 지연 발생
  2. 디버깅 어려움: 로컬 환경과 클라우드 환경의 차이로 디버깅이 복잡
  3. 실행 시간 제한: 대부분의 플랫폼에서 함수 실행 시간이 제한됨
  4. 벤더 종속성: 특정 클라우드 제공업체의 서비스에 종속될 가능성
  5. 상태 관리 복잡성: 함수는 기본적으로 상태를 유지하지 않음
  6. 모니터링 및 로깅 제한: 분산된 함수의 모니터링이 더 복잡함

8. 서버리스 환경에서의 데이터베이스 연결 처리

서버리스 함수에서 데이터베이스 연결을 효율적으로 관리하는 방법입니다.

8.1 연결 재사용

// 함수 스코프 외부에서 DB 연결 변수 선언
let connection = null;

const connectToDatabase = async () => {
  // 이미 연결이 있으면 재사용
  if (connection) {
    return connection;
  }

  // 새 연결 생성
  const mongoose = require("mongoose");
  connection = await mongoose.connect(process.env.DB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    serverSelectionTimeoutMS: 5000,
  });

  return connection;
};

exports.handler = async (event) => {
  try {
    // DB 연결
    await connectToDatabase();

    // 데이터베이스 작업 수행
    const User = mongoose.model("User", { name: String, email: String });
    const users = await User.find({});

    return {
      statusCode: 200,
      body: JSON.stringify(users),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "데이터베이스 오류" }),
    };
  }
};

9. 서버리스 아키텍처의 보안 고려사항

9.1 IAM 역할 및 권한 설정

최소 권한 원칙에 따라 함수별로 필요한 권한만 부여합니다.

# serverless.yml
functions:
  getUser:
    handler: handlers/users.getUser
    iamRoleStatements:
      - Effect: Allow
        Action:
          - dynamodb:GetItem
        Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DB_TABLE}"

9.2 환경 변수 보안

민감한 정보는 파라미터 스토어나 시크릿 매니저를 통해 안전하게 관리합니다.

const AWS = require("aws-sdk");
const ssm = new AWS.SSM();

// 파라미터 스토어에서 안전하게 시크릿 가져오기
const getSecret = async (paramName) => {
  const params = {
    Name: paramName,
    WithDecryption: true,
  };

  const response = await ssm.getParameter(params).promise();
  return response.Parameter.Value;
};

exports.handler = async (event) => {
  // 함수 실행 시 시크릿 로드
  const apiKey = await getSecret("/my-app/api-key");

  // API 키 사용
  // ...
};

10. 서버리스 아키텍처의 실제 활용 사례

  1. API 백엔드: REST 또는 GraphQL API 구현
  2. 이미지/비디오 처리: 업로드된 미디어 파일 처리
  3. CRON 작업: 정기적인 백업, 데이터 정리, 보고서 생성
  4. 실시간 알림: 웹훅, 푸시 알림, 이메일 발송
  5. IoT 데이터 처리: 센서 데이터 수집 및 분석
  6. 인증 및 인가: 토큰 발급, 사용자 인증, 권한 검증

실제 예시: 이미지 처리 서버리스 함수

const AWS = require("aws-sdk");
const sharp = require("sharp");
const s3 = new AWS.S3();

exports.handler = async (event) => {
  // S3 이벤트에서 업로드된 파일 정보 추출
  const bucket = event.Records[0].s3.bucket.name;
  const key = decodeURIComponent(
    event.Records[0].s3.object.key.replace(/\+/g, " ")
  );

  // 처리할 이미지가 아니면 종료
  if (!key.endsWith(".jpg") && !key.endsWith(".png")) {
    return { status: "skipped", key };
  }

  try {
    // 원본 이미지 가져오기
    const s3Object = await s3
      .getObject({
        Bucket: bucket,
        Key: key,
      })
      .promise();

    // 이미지 리사이징
    const resizedImageBuffer = await sharp(s3Object.Body)
      .resize(800, 600, { fit: "inside" })
      .toBuffer();

    // 처리된 이미지 저장 경로 생성
    const targetKey = `resized/${key.split("/").pop()}`;

    // 리사이징된 이미지 S3에 업로드
    await s3
      .putObject({
        Bucket: bucket,
        Key: targetKey,
        Body: resizedImageBuffer,
        ContentType: s3Object.ContentType,
      })
      .promise();

    return {
      status: "success",
      srcKey: key,
      targetKey,
    };
  } catch (error) {
    console.error("이미지 처리 오류:", error);
    return {
      status: "error",
      key,
      error: error.message,
    };
  }
};

11. 로컬 개발 및 테스트

서버리스 애플리케이션을 로컬에서 개발하고 테스트하는 도구와 방법입니다.

11.1 Serverless Offline 플러그인

# 플러그인 설치
npm install --save-dev serverless-offline

# serverless.yml에 플러그인 추가
plugins:
  - serverless-offline

# 로컬에서 실행
serverless offline

11.2 AWS SAM CLI

# SAM 템플릿 예시 (template.yaml)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: index.handler
      Runtime: nodejs14.x
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /hello
            Method: get

# 로컬 실행
sam local start-api

11.3 Lambda 함수 단위 테스트

// handler.js
module.exports.hello = async (event) => {
  const name = event.queryStringParameters?.name || "World";
  return {
    statusCode: 200,
    body: JSON.stringify({ message: `Hello, ${name}!` }),
  };
};

// handler.test.js
const { hello } = require("./handler");

test("hello 함수 테스트", async () => {
  const event = {
    queryStringParameters: { name: "홍길동" },
  };

  const response = await hello(event);
  const body = JSON.parse(response.body);

  expect(response.statusCode).toBe(200);
  expect(body.message).toBe("Hello, 홍길동!");
});

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

  1. 함수 크기 최소화: 필요한 모듈만 포함하여 콜드 스타트 시간 단축
  2. 연결 재사용: 데이터베이스 연결 등을 함수 컨텍스트 외부에서 관리
  3. 모니터링 강화: CloudWatch 등을 사용하여 오류 및 성능 모니터링
  4. 타임아웃 설정: 함수 특성에 맞는 적절한 타임아웃 설정
  5. 메모리 할당 최적화: 실행 특성에 맞게 메모리 할당량 조정
  6. 비동기 처리 활용: I/O 작업은 가능한 비동기로 처리

요약

Node.js에서 서버리스 아키텍처를 구현하면 인프라 관리 없이 확장 가능한 애플리케이션을 빠르게 개발할 수 있습니다. AWS Lambda, Azure Functions, Google Cloud Functions 등의 플랫폼을 통해 Node.js 기반 서버리스 함수를 쉽게 배포하고 관리할 수 있으며, Serverless Framework 같은 도구를 사용하면 개발 프로세스가 더욱 간소화됩니다.

서버리스는 인프라 관리가 필요 없고, 자동 확장성과 비용 효율성이 뛰어나다는 장점이 있지만, 콜드 스타트 지연, 벤더 종속성, 실행 시간 제한 등의 단점도 존재합니다. 따라서 애플리케이션의 특성과 요구사항을 고려하여 서버리스 아키텍처의 적합성을 판단해야 합니다.

API 백엔드, 이미지 처리, 정기 작업, 실시간 알림 등 다양한 사용 사례에서 서버리스 아키텍처는 강력한 솔루션이 될 수 있으며, Node.js의 비동기적 특성과 가벼운 실행 환경은 서버리스 패러다임과 매우 잘 어울립니다.

results matching ""

    No results matching ""