Post

AWS Lambda - 서버리스 컴퓨팅의 핵심

AWS Lambda의 개념부터 실전 활용까지 완벽 가이드

AWS Lambda란?

AWS Lambda는 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있는 서버리스 컴퓨팅 서비스임.

이벤트에 응답하여 코드를 실행하고, 컴퓨팅 리소스를 자동으로 관리함. 사용한 컴퓨팅 시간에 대해서만 요금이 부과되며, 코드가 실행되지 않을 때는 요금이 발생하지 않음.


왜 Lambda를 사용해야 하는가?

기존 서버 기반 방식의 문제점

전통적인 EC2 기반 아키텍처:

1
2
3
4
5
6
7
┌─────────────────────────────────────┐
│  EC2 인스턴스 (24시간 운영)         │
│  - 최소 사양: t3.small             │
│  - 월 비용: ~$15                   │
│  - 실제 사용률: 5%                 │
│  - 95% 시간 동안 유휴 상태         │
└─────────────────────────────────────┘

문제점:

  1. 고정 비용: 사용하지 않아도 서버 비용 발생
  2. 관리 부담: OS 패치, 보안 업데이트, 모니터링 필요
  3. 확장성 제한: 트래픽 급증 시 수동으로 인스턴스 추가 필요
  4. 리소스 낭비: 트래픽이 적을 때도 서버는 계속 실행됨
  5. 복잡한 배포: 서버 설정, 로드 밸런서 구성 등 필요

Lambda의 해결 방법

1
2
3
4
5
6
7
┌─────────────────────────────────────┐
│  Lambda 함수                        │
│  - 실행 시에만 과금                 │
│  - 월 비용: ~$0.20 (동일 부하 가정)│
│  - 자동 스케일링                    │
│  - 서버 관리 불필요                 │
└─────────────────────────────────────┘

장점:

  1. 종량제 과금: 실제 사용한 시간만큼만 비용 지불 (100ms 단위)
  2. 제로 관리: 서버 관리, 패치, 확장 모두 AWS가 담당
  3. 자동 스케일링: 요청 수에 따라 자동으로 확장/축소
  4. 고가용성: 여러 가용 영역에 자동으로 분산
  5. 빠른 배포: 코드만 업로드하면 즉시 실행 가능

Lambda의 핵심 개념

1. 함수 (Function)

Lambda의 기본 실행 단위임. 특정 이벤트에 응답하여 실행되는 코드 조각.

기본 구조 (Node.js 예시):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const handler = async (event, context) => {
  // event: 함수를 트리거한 이벤트 데이터
  // context: 런타임 정보 (requestId, 남은 시간 등)

  console.log('Received event:', JSON.stringify(event, null, 2));

  // 비즈니스 로직 수행
  const result = await processData(event);

  // 응답 반환
  return {
    statusCode: 200,
    body: JSON.stringify(result)
  };
};

2. 트리거 (Trigger)

Lambda 함수를 실행시키는 이벤트 소스.

주요 트리거 유형:

트리거 유형사용 사례실전 예시
API GatewayHTTP 요청 처리REST API, 웹 애플리케이션 백엔드
S3파일 업로드/삭제이미지 리사이징, CSV 처리
DynamoDB Streams데이터 변경 감지실시간 데이터 동기화
SQS메시지 큐 처리비동기 작업 처리
Kinesis스트리밍 데이터IoT 센서 데이터 처리
EventBridge스케줄/이벤트주기적 작업, 이벤트 라우팅
CloudWatch Logs로그 분석실시간 로그 모니터링

3. 실행 환경 (Execution Environment)

Lambda 함수가 실행되는 격리된 환경.

콜드 스타트 vs 웜 스타트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
콜드 스타트 (Cold Start):
┌────────────────────────────────────┐
│ 1. 실행 환경 생성 (100-500ms)      │
│ 2. 런타임 초기화 (50-200ms)        │
│ 3. 코드 다운로드 (10-100ms)        │
│ 4. 핸들러 초기화 (10-500ms)        │
│ 5. 함수 실행 (사용자 코드)         │
└────────────────────────────────────┘
총 시간: 200ms - 1300ms + 실행 시간

웜 스타트 (Warm Start):
┌────────────────────────────────────┐
│ 1. 함수 실행 (사용자 코드)         │
└────────────────────────────────────┘
총 시간: 실행 시간만

콜드 스타트 최적화 방법:

  1. 최소 종속성: 필요한 라이브러리만 포함
  2. 코드 크기 최소화: 불필요한 파일 제거
  3. Provisioned Concurrency: 미리 웜 인스턴스 유지 (비용 증가)
  4. 전역 변수 활용: 연결 객체는 핸들러 밖에서 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 나쁜 예 - 매번 새로운 클라이언트 생성
export const handler = async (event) => {
  const dynamoClient = new DynamoDBClient(); // 콜드/웜 모두 매번 실행
  // ...
};

// ✅ 좋은 예 - 재사용 가능한 클라이언트
const dynamoClient = new DynamoDBClient(); // 한 번만 실행 (재사용)

export const handler = async (event) => {
  // 클라이언트 재사용
  await dynamoClient.send(command);
};

실전 프로젝트 활용 사례

사례 1: 이미지 자동 처리 파이프라인

아키텍처:

1
2
3
4
5
사용자 → S3 Upload → EventBridge → Lambda (Image Processor)
                                   ├─ Thumbnail 생성
                                   ├─ Resized 이미지 생성
                                   ├─ 메타데이터 추출
                                   └─ DynamoDB 저장

Lambda 함수 코드 (핵심 부분):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import sharp from 'sharp';

const s3Client = new S3Client({});

export const handler = async (event) => {
  // EventBridge에서 S3 업로드 이벤트 수신
  const bucket = event.detail.bucket.name;
  const key = event.detail.object.key;

  // 원본 이미지 다운로드
  const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
  const { Body } = await s3Client.send(getCommand);
  const imageBuffer = await streamToBuffer(Body);

  // Sharp로 썸네일 생성 (200x200)
  const thumbnail = await sharp(imageBuffer)
    .resize(200, 200, { fit: 'cover' })
    .jpeg({ quality: 80 })
    .toBuffer();

  // S3에 저장
  const putCommand = new PutObjectCommand({
    Bucket: bucket,
    Key: `thumbnails/${key}`,
    Body: thumbnail,
    ContentType: 'image/jpeg'
  });
  await s3Client.send(putCommand);

  return { message: 'Image processed successfully' };
};

장점:

  • S3 업로드와 동시에 자동으로 처리
  • 이미지 수에 관계없이 자동 확장
  • 처리 시간만큼만 과금 (약 1-2초/이미지)

사례 2: 비동기 주문 처리 시스템

아키텍처:

1
2
3
4
5
6
7
API Gateway → Lambda (Order Creator)
            → SQS Queue
            → Lambda (Order Worker) [배치 10개]
            ├─ 재고 확인
            ├─ 결제 처리
            ├─ DynamoDB 저장
            └─ SNS 알림

Order Worker Lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export const handler = async (event) => {
  const records = event.Records; // SQS 메시지 배치 (최대 10개)
  const results = { successful: [], failed: [] };

  for (const record of records) {
    const order = JSON.parse(record.body);

    try {
      // 1. 재고 확인 (10% 실패율 시뮬레이션)
      await checkInventory(order.items);

      // 2. 결제 처리 (5% 실패율 시뮬레이션)
      const payment = await processPayment(order);

      // 3. 주문 저장
      await saveOrder(order, payment);

      // 4. 알림 전송
      await sendNotification(order);

      // 성공 시 SQS 메시지 삭제
      await sqsClient.send(new DeleteMessageCommand({
        QueueUrl: QUEUE_URL,
        ReceiptHandle: record.receiptHandle
      }));

      results.successful.push(order.orderId);

    } catch (error) {
      console.error(`Order ${order.orderId} failed:`, error);
      results.failed.push(order.orderId);
      // 메시지를 삭제하지 않으면 자동으로 재시도됨
    }
  }

  console.log(`Processed: ${results.successful.length} success, ${results.failed.length} failed`);
  return results;
};

SQS 통합의 장점:

  1. 자동 배치 처리: 10개 메시지를 한 번에 처리 (비용 절감)
  2. 자동 재시도: 실패 시 메시지가 큐에 남아 재처리됨
  3. DLQ (Dead Letter Queue): 3번 실패 후 DLQ로 이동
  4. 부하 분산: 메시지 수에 따라 Lambda 인스턴스 자동 증가

사례 3: 실시간 IoT 센서 데이터 처리

아키텍처:

1
2
3
4
5
IoT Device → Kinesis Data Stream → Lambda (Batch 100개)
                                  ├─ DynamoDB (Raw Data)
                                  ├─ DynamoDB (1분 집계)
                                  ├─ CloudWatch Metrics
                                  └─ SNS 알림 (임계값 초과 시)

센서 데이터 처리 Lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
export const handler = async (event) => {
  const records = event.Records; // Kinesis 레코드 배치 (최대 100개)

  for (const record of records) {
    // Base64 디코딩
    const payload = Buffer.from(record.kinesis.data, 'base64').toString();
    const sensorData = JSON.parse(payload);

    // 1. 원시 데이터 저장
    await saveSensorData(sensorData);

    // 2. CloudWatch 메트릭 전송
    await sendCloudWatchMetrics(sensorData);

    // 3. 임계값 체크
    if (sensorData.temperature > 30 || sensorData.humidity > 80) {
      await publishAlert(sensorData);
    }

    // 4. 1분 단위 집계 업데이트
    await updateAggregation(sensorData);
  }

  console.log(`Processed ${records.length} sensor readings`);
};

// 1분 단위 집계 (원자적 업데이트)
async function updateAggregation(data) {
  const period = normalizeToMinute(data.timestamp);

  await dynamoClient.send(new UpdateCommand({
    TableName: AGGREGATION_TABLE,
    Key: { deviceId: data.deviceId, aggregationPeriod: period },
    UpdateExpression: `
      SET #count = if_not_exists(#count, :zero) + :one,
          sumTemperature = if_not_exists(sumTemperature, :zero) + :temp,
          minTemperature = if_not_exists(minTemperature, :temp),
          maxTemperature = if_not_exists(maxTemperature, :temp)
    `,
    ExpressionAttributeNames: { '#count': 'count' },
    ExpressionAttributeValues: {
      ':zero': 0,
      ':one': 1,
      ':temp': data.temperature
    }
  }));
}

Kinesis 통합의 장점:

  1. 순서 보장: 동일 디바이스의 데이터는 순서대로 처리
  2. 고속 처리: 100개 레코드를 한 번에 배치 처리
  3. 내결함성: 샤드 장애 시 자동 재시도
  4. 병렬 처리: 샤드별로 병렬 Lambda 실행

Lambda 구성 옵션

1. 메모리 및 성능

1
2
3
4
5
functions:
  myFunction:
    handler: index.handler
    memorySize: 512  # 128MB - 10,240MB (1MB 단위)
    timeout: 30      # 최대 900초 (15분)

메모리와 CPU의 관계:

  • 메모리가 증가하면 CPU도 비례해서 증가
  • 1,769MB = 1 vCPU
  • 10,240MB = 약 6 vCPU

메모리 최적화 예시:

1
2
3
4
테스트 결과:
- 128MB: 실행 시간 5000ms, 비용 $0.000083
- 512MB: 실행 시간 1500ms, 비용 $0.000025 ✅ (최적)
- 1024MB: 실행 시간 800ms, 비용 $0.000027

512MB가 가장 저렴한 이유: 빠른 실행으로 총 컴퓨팅 시간 감소

2. 환경 변수

1
2
3
4
5
6
7
functions:
  myFunction:
    environment:
      TABLE_NAME: ${self:custom.tableName}
      QUEUE_URL: !Ref MyQueue
      API_KEY: ${env:SECRET_API_KEY}  # .env 파일에서
      STAGE: ${self:provider.stage}

보안 주의사항:

  • 민감한 정보는 AWS Secrets Manager 또는 Parameter Store 사용
  • Lambda 콘솔에서 환경 변수가 노출되므로 비밀번호는 직접 입력 금지

3. IAM 권한 (최소 권한 원칙)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
provider:
  iam:
    role:
      statements:
        # ✅ 좋은 예: 특정 리소스만 접근
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
          Resource: !GetAtt MyTable.Arn

        # ❌ 나쁜 예: 와일드카드 사용
        - Effect: Allow
          Action:
            - dynamodb:*
          Resource: "*"

4. VPC 구성 (선택사항)

VPC Lambda 사용 시나리오:

  • RDS 데이터베이스 접근
  • ElastiCache 사용
  • 프라이빗 서브넷의 리소스 접근
1
2
3
4
5
6
7
8
functions:
  myFunction:
    vpc:
      securityGroupIds:
        - sg-0abc123def456
      subnetIds:
        - subnet-0abc123
        - subnet-0def456

VPC Lambda의 단점:

  • 콜드 스타트 증가 (ENI 생성 시간)
  • NAT Gateway 비용 발생 (인터넷 접근 시)

해결책:

  • VPC Endpoints 사용 (S3, DynamoDB 등)
  • Hyperplane ENI (2019년 이후 콜드 스타트 개선)

Lambda vs 전통적 서버 비교

시나리오: 주문 처리 API

EC2 기반 아키텍처:

1
2
3
4
5
6
7
8
9
10
11
12
비용 분석 (월 평균 10만 요청):
- EC2 t3.small (2대, 고가용성): $30/월
- ALB (로드 밸런서): $22/월
- EBS 스토리지: $10/월
- CloudWatch 모니터링: $3/월
총 비용: $65/월

추가 고려사항:
- 서버 관리 시간: 월 4시간
- 보안 패치 및 업데이트 필요
- 오토 스케일링 설정 복잡
- 트래픽 급증 시 대응 지연

Lambda 기반 아키텍처:

1
2
3
4
5
6
7
8
9
10
11
비용 분석 (월 평균 10만 요청, 평균 500ms 실행):
- Lambda 요청 비용: $0.02
- Lambda 컴퓨팅 비용: $0.83
- API Gateway: $0.35
총 비용: $1.20/월

추가 고려사항:
- 서버 관리 불필요
- 자동 스케일링
- 트래픽 급증 즉시 대응
- 고가용성 기본 제공

비용 절감: 98% ($65 → $1.20)


Lambda 제약사항 및 해결 방법

제약사항

제약한계해결 방법
실행 시간최대 15분Step Functions로 장기 워크플로우 오케스트레이션
메모리최대 10GB큰 파일은 S3에서 스트리밍 처리
배포 패키지50MB (압축), 250MB (압축 해제)Lambda Layers 사용, 종속성 최소화
/tmp 디스크최대 10GB임시 파일은 S3 사용
동시 실행계정당 1,000개 (기본)한도 증가 요청 또는 Reserved Concurrency

실행 시간 제약 해결: Step Functions 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 15분 이상 걸리는 CSV 처리
stepFunctions:
  stateMachines:
    csvProcessor:
      definition:
        StartAt: ValidateCSV
        States:
          ValidateCSV:
            Type: Task
            Resource: !GetAtt ValidatorLambda.Arn
            Next: TransformData
            Retry:
              - ErrorEquals: [States.Timeout]
                MaxAttempts: 2

          TransformData:
            Type: Task
            Resource: !GetAtt TransformerLambda.Arn
            Next: LoadData

          LoadData:
            Type: Task
            Resource: !GetAtt LoaderLambda.Arn
            End: true

각 단계는 15분 내에 완료되지만, 전체 워크플로우는 1시간 이상 걸릴 수 있음.


Lambda 모니터링 및 디버깅

CloudWatch Logs

Lambda는 자동으로 CloudWatch Logs에 로그를 전송함.

구조화된 로깅 패턴:

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 나쁜 예
console.log('Processing order');

// ✅ 좋은 예
console.log(JSON.stringify({
  level: 'INFO',
  message: 'Processing order',
  orderId: order.id,
  customerId: order.customerId,
  timestamp: new Date().toISOString(),
  requestId: context.requestId
}));

CloudWatch Logs Insights 쿼리:

1
2
3
4
fields @timestamp, level, message, orderId, customerId
| filter level = "ERROR"
| sort @timestamp desc
| limit 100

CloudWatch Metrics

커스텀 메트릭 전송:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';

const cwClient = new CloudWatchClient({});

async function publishMetric(metricName, value, dimensions) {
  await cwClient.send(new PutMetricDataCommand({
    Namespace: 'MyApplication',
    MetricData: [{
      MetricName: metricName,
      Value: value,
      Unit: 'Count',
      Timestamp: new Date(),
      Dimensions: dimensions
    }]
  }));
}

// 사용 예시
await publishMetric('OrderProcessed', 1, [
  { Name: 'Status', Value: 'Success' },
  { Name: 'Region', Value: 'ap-northeast-2' }
]);

X-Ray를 통한 분산 추적

1
2
3
4
5
6
7
provider:
  tracing:
    lambda: true  # X-Ray 추적 활성화

functions:
  myFunction:
    handler: index.handler

X-Ray SDK 사용:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import AWSXRay from 'aws-xray-sdk-core';
const AWS = AWSXRay.captureAWS(require('aws-sdk'));

export const handler = async (event) => {
  // 서브세그먼트로 세분화된 추적
  const segment = AWSXRay.getSegment();
  const subsegment = segment.addNewSubsegment('DatabaseQuery');

  try {
    const result = await queryDatabase();
    subsegment.close();
    return result;
  } catch (error) {
    subsegment.addError(error);
    subsegment.close();
    throw error;
  }
};

Lambda 비용 최적화 전략

1. Graviton2 프로세서 사용 (ARM64)

1
2
3
4
functions:
  myFunction:
    handler: index.handler
    architecture: arm64  # 20% 비용 절감

x86_64 대비 20% 저렴하고 성능도 유사하거나 더 좋음.

2. 메모리 크기 최적화

AWS Lambda Power Tuning 도구 사용:

1
npm install -g aws-lambda-power-tuning

자동으로 여러 메모리 크기를 테스트하여 최적의 구성 찾아줌.

3. 불필요한 종속성 제거

1
2
3
4
5
// ❌ 전체 AWS SDK 임포트 (100MB+)
const AWS = require('aws-sdk');

// ✅ 필요한 클라이언트만 임포트 (5MB)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

4. Reserved Concurrency 활용

예측 가능한 트래픽이 있다면:

1
2
3
functions:
  myFunction:
    reservedConcurrency: 10  # 항상 10개 인스턴스 예약

콜드 스타트 감소, 하지만 비용 증가 (idle 시간에도 예약됨).


Lambda 배포 전략

1. 버전 및 별칭

1
2
3
4
5
6
7
8
functions:
  myFunction:
    handler: index.handler

# 배포 시 자동으로 버전 생성
deploy:
  function:
    versioning: true

별칭을 통한 트래픽 라우팅:

1
2
3
4
5
6
7
8
# 새 버전 배포
aws lambda publish-version --function-name myFunction

# 별칭 업데이트 (50% 트래픽을 새 버전으로)
aws lambda update-alias \
  --function-name myFunction \
  --name production \
  --routing-config AdditionalVersionWeights={"2"=0.5}

2. 카나리 배포

1
2
3
4
5
6
7
functions:
  myFunction:
    handler: index.handler
    deploymentSettings:
      type: Canary10Percent5Minutes
      alarms:
        - FunctionErrorAlarm

10%씩 트래픽을 증가시키며 5분마다 검증.


실전 팁 및 Best Practices

1. 재사용 가능한 연결 객체

1
2
3
4
5
6
7
// SDK 클라이언트는 핸들러 외부에서 생성
const dynamoClient = new DynamoDBClient({});
const s3Client = new S3Client({});

export const handler = async (event) => {
  // 재사용
};

2. 환경별 구성 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
custom:
  stages:
    dev:
      memorySize: 512
      timeout: 30
    prod:
      memorySize: 1024
      timeout: 60

functions:
  myFunction:
    memorySize: ${self:custom.stages.${self:provider.stage}.memorySize}
    timeout: ${self:custom.stages.${self:provider.stage}.timeout}

3. 에러 핸들링 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const handler = async (event) => {
  try {
    const result = await processEvent(event);
    return {
      statusCode: 200,
      body: JSON.stringify(result)
    };
  } catch (error) {
    console.error('Error processing event:', {
      error: error.message,
      stack: error.stack,
      event: JSON.stringify(event)
    });

    // 재시도 가능한 에러는 throw (SQS, Kinesis 등)
    if (error.retryable) {
      throw error;
    }

    // 재시도 불가능한 에러는 로그만 남기고 성공 반환
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal Server Error' })
    };
  }
};

4. 동시성 제어

1
2
3
4
functions:
  myFunction:
    reservedConcurrency: 5  # 최대 5개 동시 실행
    # 외부 API 레이트 리밋 준수

5. 데이터베이스 연결 관리

RDS 같은 연결 기반 DB 사용 시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import mysql from 'mysql2/promise';

let connection;

async function getConnection() {
  if (!connection) {
    connection = await mysql.createConnection({
      host: process.env.DB_HOST,
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      connectionLimit: 1  # Lambda는 단일 연결로 충분
    });
  }
  return connection;
}

export const handler = async (event) => {
  const conn = await getConnection();
  const [rows] = await conn.execute('SELECT * FROM users WHERE id = ?', [event.userId]);
  return rows;
};

또는 RDS Proxy 사용 (권장):

  • 연결 풀링 자동 관리
  • Lambda 동시성에 따른 연결 폭증 방지

마치며

AWS Lambda는 서버리스 컴퓨팅의 핵심 서비스로, 다음과 같은 상황에서 특히 유용함:

Lambda를 사용해야 할 때:

  • 이벤트 기반 처리 (파일 업로드, 메시지 큐 등)
  • 간헐적으로 실행되는 작업
  • 트래픽 변동이 큰 애플리케이션
  • 빠른 프로토타이핑 및 개발
  • 마이크로서비스 아키텍처

Lambda 대신 다른 방식을 고려해야 할 때:

  • 15분 이상 실행되는 작업 (→ ECS, Fargate)
  • 지속적으로 높은 트래픽 (→ EC2 Auto Scaling이 더 저렴할 수 있음)
  • 복잡한 상태 관리 필요 (→ Kubernetes)
  • WebSocket 장시간 연결 (→ EC2, ECS)

실전 프로젝트에서 Lambda는 S3, DynamoDB, SQS, SNS 등 다른 AWS 서비스와 조합하여 강력한 이벤트 기반 아키텍처를 구축할 수 있음.

적절한 메모리 크기, 타임아웃, 배치 크기 조정을 통해 비용과 성능을 최적화할 수 있으며, CloudWatch와 X-Ray를 통한 모니터링으로 안정적인 운영이 가능함.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.