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. 서버리스 아키텍처의 장점
운영 비용 절감: 사용한 컴퓨팅 리소스에 대해서만 비용을 지불합니다.
자동 확장성: 트래픽 증가에 따라 자동으로 확장됩니다.
빠른 배포: 개발에서 프로덕션까지의 시간이 단축됩니다.
관리 오버헤드 감소: 서버 관리에 신경 쓸 필요가 없어 개발에 집중할 수 있습니다.
내장된 고가용성: 대부분의 서버리스 플랫폼은 기본적으로 고가용성을 제공합니다.
9. 서버리스 아키텍처의 단점
- 콜드 스타트: 함수가 일정 시간 호출되지 않으면 다시 시작할 때 지연이 발생합니다.
// 콜드 스타트 최소화를 위한 전략
// 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) };
};
실행 시간 제한: 대부분의 플랫폼은 함수 실행 시간에 제한이 있습니다.
상태 비저장: 함수 간 상태 공유가 어렵습니다.
디버깅 및 테스트의 복잡성: 클라우드 환경에서의 디버깅이 더 어려울 수 있습니다.
비용 예측 어려움: 사용량이 많아지면 경우에 따라 기존 서버 기반 모델보다 비용이 증가할 수 있습니다.
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 등 다양한 용도에 적합하지만, 콜드 스타트, 실행 시간 제한, 상태 관리의 어려움 등의 단점도 있습니다. 이러한 제약사항을 이해하고 적절한 설계 원칙을 적용하면 효율적인 서버리스 애플리케이션을 구축할 수 있습니다.
서버리스는 모든 상황에 적합한 만능 솔루션이 아니므로, 애플리케이션의 요구사항과 특성을 고려하여 기존 서버 기반 아키텍처와 서버리스 아키텍처 중 적절한 것을 선택하는 것이 중요합니다.