반응형

이번 포스트에서는 랜덤 문제 풀이를 구현하면서 "랜덤한 순서"는 되어도 "랜덤한 문제"는 아니었던  저의 단순한 사고의 개발 과정과 고민했던 내용들을 정리해보았습니다. 


단순한 사고 ONE

프론트: 범블비님 단어 퀴즈 API에서 랜덤 기능 추가해 주세요~

범블비: 네네~ 금방 만들겠습니다!

 

기존에 "퀴즈 조회 API에서 랜덤 요청 필터가 들어오면 단어를 한 번 섞어서 리턴하면 되겠다!"라고 생각하고 개발을 진행했습니다.

프론트에서 limit, page, ... 여러 필터 파라미터를 받아서 그 기준으로 데이터를 조회하고 배열에 담아서 Fisher-Yates 알고리즘으로 한 번 뒤죽박죽 섞어주면 될거라고 생각했습니다

// Fisher-Yates 알고리즘
  shuffleArray<T>(array: T[]): T[] {
    {
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
      return array;
    }
  }

 

그런데 여기서 놓친 부분이 있었습니다. 위와 같이 DB에서 조회한 데이터를 기준으로 배열의 순서를 바꾸는 건 "랜덤한 순서"는 되어도 "랜덤한 문제"는 아니었던것이죠


단순한 사고 TWO

데이터부터 랜덤하게 조회하기 위해서 orderBy에 RAND()를 사용해서 랜덤한 문제를 뽑아오고, 페이지네이션을 통해 나눠서 보여주는 구조로 수정하였습니다.

처음에는 단순하게 seed를 넘겨받아 사용하도록 구성하였습니다.

"seed는 단어 섞는 '열쇠' 같은 거예요.
같은 seed로 요청하면 항상 같은 순서로 단어가 섞여요.
퀴즈 시작할 땐 새로운 seed로 요청하고, 퀴즈 중엔 계속 그 seed로 요청하면 돼요."
queryBuilder.orderBy('RAND()', 'ASC').take(limit).skip(offset);
 
queryBuilder.orderBy('RAND(:seed)', 'ASC').setParameters({ seed });

 

이렇게 하면 seed 기반으로 랜덤 정렬도 되고, offset/limit으로 페이징도 되니까 괜찮다고 생각했습니다.

 

그런데 위 방식에도 예상치 못한 문제가 있었습니다.

  • LIMIT 5만 필요해도 → 전체 row를 메모리에 올리고 계산하고 정렬해야 함
  • 특히 인덱스를 사용할 수 없어서 풀스캔 + 정렬 연산이 발생
  • 데이터가 많거나 동시에 많은 사용자가 호출하면 DB에 큰 부하가 발생

조금 더 고민해보자,,

  • 랜덤 셔플은 유지하면서
  • 페이지네이션으로 중복 없이 순서대로 문제를 보여주고
  • 사용자마다 동일한 시드(seed)로 요청하면 항상 같은 순서로 퀴즈가 진행되며
  • 성능적으로도 문제없는 구조를 만들 수 없을까?

SortIndex를 미리 계산해두는 방식

랜덤한 순서를 미리 계산해서 정렬 기준으로 저장해두고, 이후에는 그냥 그걸 기준으로 페이지네이션해서 가져오는 방식

퀴리 예시)
SELECT * FROM question
WHERE userId = 123
ORDER BY sortIndex ASC
LIMIT 5 OFFSET 0;

이 방식을 쓰기 위한 준비

1. question 테이블에 sortIndex 컬럼 추가

 

ALTER TABLE question ADD COLUMN sortIndex INT;

 

 

2. 퀴즈 시작 시 서버에서 사용자 데이터를 랜덤하게 정렬

 

await this.questionRepository.query(`
  UPDATE question
  SET sortIndex = FLOOR(RAND(:seed) * 1000000)
  WHERE user_id = :userId
`, { seed, userId });

 

  • 이 작업은 퀴즈 시작할 때 딱 한 번만 실행
  • 이후에는 sortIndex만 정렬 기준으로 사용

3. 실제 조회 쿼리에서는

queryBuilder.orderBy('question.sortIndex', 'ASC');
queryBuilder.take(limit).skip(offset);

이렇게 하면 RAND()를 실시간으로 돌리지 않아도 되니까 랜덤 정렬을 미리 계산해두는 전략이 성능 향상에 더 효과적입니다.


마무리

단순하게 생각하고 개발해서 랜덤인 척!?해서 사용자 경험을 해칠 수 있었는데 리턴되는 응답 값이 이상함을 느끼고 문제를 빠르게 파악해서 "랜덤 한 순서"가 아닌 "랜덤 한 문제"로 수정하고 성능까지 신경 써서 개발하는 경험을 하였습니다.

사용자들이 느낄 수 없을 수도 있지만 작지만 소중한 경험을 제공해 드린 것 같아서 뿌듯한 경험이 추가되었습니다!

728x90
반응형
반응형

이번 포스트에서는 제가 실제로 겪었던 Node.js 버전 차이로 인한 장애 사례와 이를 어떻게 대응하고 개선했는지를 정리해보려 합니다.
누군가에게는 비슷한 문제를 사전에 예방하는 데 도움이 되었으면 좋겠습니다.


장애 발생 배경

프로젝트에서 OneSignal 태그 API를 연동하는 작업을 진행하던 중, 개발 환경에서는 아무 문제없이 잘 작동하던 API가 운영 환경에서 주기적으로 서버가 죽는 현상이 발생했습니다.

초기에는 로직 오류나 외부 API 문제를 의심했지만, 로그를 살펴보며 원인을 좁혀나간 끝에 문제의 핵심은 Node.js의 버전 차이였습니다.


원인 분석

문제는 코드 내에서 사용한 fetch API였습니다.

  • 개발 환경(Node 18): fetch가 기본 내장되어 있어 문제없이 작동.
  • 운영 환경(Node 16): fetch가 내장되어 있지 않음 → 런타임에서 ReferenceError: fetch is not defined 발생 → 서버 크래시.

즉, 코드 자체는 문제 없었지만 운영 서버의 Node.js 버전에서는 fetch를 지원하지 않아 장애가 발생했던 것이죠,,  😭 😭

 


1차 대응: httpService로 긴급 대체

장애가 발생한 당시, 가장 빠르게 문제를 해결하기 위해 코드를 롤백해서 OneSignal API 호출하는 부분을 모두 제거했습니다.
그리고 fetch를 사용하는 부분을 NestJS에서 제공하는 HttpService로 빠르게 전환 후 hotfix로 수정했습니다.

// 기존 
await fetch('https://api.onesignal.com'); 
// 수정 
await this.httpService.axiosRef.post('https://api.onesignal.com', {});


이렇게 수정 후에는 더 이상 서버가 죽지 않았고, 장애는 일단락되었습니다.


2차 대응: Node.js 버전 업그레이드

정말 부끄럽고 기본이 부족한 이유이지만 이번 장애의 궁극적인 원인은 개발 서버 Node 버전과 운영 서버의 Node 버전의 싱크를 맞추지 않고 운영한 것에서 발생한 문제라고 생각하여서 장기적으로는 fetch뿐 아니라 다른 최신 기능도 사용할 수 있도록 운영 서버의 Node.js 버전을 18로 업그레이드해서 개발 서버 버전과 통일시켰습니다.

업그레이드 이후에는 fetch도 다시 사용할 수 있게 되었고, 개발 서버에서 발생하지 않는 문제가 운영 서버 전파되지 않도록 구성하였습니다.


3차 대응: ECS 배포 파이프라인 테스트 단계 추가

이번 장애를 계기로 배포 전 테스트 코드의 중요성을 다시 한번 느꼈습니다. 그래서 후속 조치로 ECS 배포 파이프라인에 테스트 코드 검증 단계를 추가했습니다.
배포 전에 코드가 현재 운영 Node.js 환경에서 문제없이 동작하는지 체크하도록 설정하여, 환경 차이로 인한 문제를 사전에 발견할 수 있도록 개선했습니다.


마무리하며

이번 장애는 기본중에 기본인,, 개발과 운영 환경을 일치 시키지 않는 말도 안 되는 문제에서 시작되었지만, 덕분에 시스템 안정성과 배포 프로세스를 더욱 견고하게 만드는 계기가 되었습니다.

환경 차이에 대한 고려, 빠른 롤백/우회 처리, 지속적인 테스트 자동화는 앞으로도 계속 챙겨야 할 중요한 부분이라는 것을 다시 느꼈습니다.

혹시 비슷한 환경에서 개발하고 계신 분이라면, 운영 환경의 Node.js 버전도 꼭 체크해 보세요 🥲

추가적으로 fetch 사용하려고 했던 이유는

  • fetch 사용:
    - fetch는 HTTP 응답 자체 (Response 객체)만 반환합니다.
const response = await fetch(url, options); 
const { recipients, success } = await response.json(); // 바로 구조 분해 가능
  • HttpService 사용:
    - Axios는 AxiosResponse 형태로 응답
const response = await this.httpService.axiosRef.get(url, { });
const { recipients, success } = response.data;
{
  data: {...},         // 실질적인 응답 JSON
  status: 200,
  statusText: 'OK',
  headers: {...},
  config: {...},
  request: {...}
}


응답 데이터 구조가 더 단순하게 다가오기 때문에 사용하려고 했는데 이 여파가 장애로 이어지다니,,,

 

 

728x90
반응형
반응형

이번 포스트에서는 Android 인앱결제 구독 동기화를 위한 Google RTDN (Real-time Developer Notifications) 개발 과정과 고민했던 내용들을 정리해보았습니다.

이전 글에서 공유한 iOS ASN 개발기에 이어, Android에서도 실시간 구독 상태 동기화가 필요해 RTDN을 도입하게 되었어요.


왜 RTDN을 도입했을까?

Google API를 통해 수동으로 구독 상태를 검증하고 있었습니다. 하지만 이 방식은 앱 접속 시점에만 동기화가 가능해 실시간성이 부족했습니다.

  • 앱을 켜지 않으면 구독 상태 동기화 불가능
  • 자동갱신/ 유예/ 취소 등의 상태 반영 지연 같은 문제로 실시간성이 떨어졌습니다.

이를 해결하고자 Google에서 제공하는 RTDN을 도입하게 되었습니다.


인프라 구성: Google RTDN → Pub/Sub → Backend

Google은 RTDN을 Pub/Sub 방식으로 제공합니다. 이 메시지를 백엔드에 안전하게 전달하기 위해 아래 구조로 구성했습니다:

Google Play RTDN → Google Cloud Pub/Sub → Backend 서버
  • Pub/Sub: Google이 메시지를 발행하는 통로
  • Backend: 구독 상태 업데이트 및 DB 반영

개발 중 겪었던 고민들

1. Pub/Sub 환경 구성

  • IOS와 아키텍처를 통일해서 Pub/Sub  AWS API Gateway → SQS를 구조를 구성해야 하나 고민을 하였지만 Pub/Sub도 충분 히 신뢰성 있는 메시징 서비스이고, 데드 레터 처리가 가능해서 Gateway → SQS 구조를 과감하게 배제

2. 환불(Refund) RTDN 이슈

  • voidedPurchaseNotification 값 뿐만아니라 SUBSCRIPTION_REVOKED 상태값에서도 환불이 들어옴

3. 테스트 환경 RTDN 수신 문제

  • Google은 Push 구독의 엔드포인트를 한 개만 운영 등록 가능하기 때문에, 개발 환경에서는 RTDN 수신 테스트가 어려움.
  • Pub/Sub의 구독을 개발용으로 생성해서 개발 엔드포인트에도 동시에 푸시 받는 구조로 해결.

구현 포인트 정리

  • notificationType 기반으로 상태 분기 처리
    → SUBSCRIPTION_RENEWED, CANCELED, EXPIRED 등
  • subscriptionState 기반으로 보조 판단
    → 실제 상태가 IN_GRACE_PERIOD 인데 RENEWED라고 오면 이중 체크
  • voidedPurchaseNotification 수신 시 환불 처리
  • 구독권 비교 시 orderId 기준으로 최신 구독 여부 판단
    → 기존 DB orderId ≠ RTDN orderId → 상태 재조회 + 동기화
  • 테스트 결제 구분 (res.testPurchase 확인 후 production 에선 무시)

마무리

향후에 메시징 서비스를 한쪽으로 통합해서 IOS, AOS 결제를 한 번 가공해서 백엔드 엔드 포인트로 넘기는 구조로 개선한다면 백엔드 비즈니스 코드가 유지 보수성이 개선될 수도 있을 것 같습니다!

728x90
반응형
반응형


이번 포스트에서는 제가 실제로 경험한 iOS 인앱결제(IAP)와 Apple Server Notification(ASN) 개발 과정에서의 고민과 해결 방법을 정리해 보려고 합니다. 특히, 실시간 구독 상태를 동기화하기 위해 ASN을 백엔드에 통합하는 과정에 집중했습니다.


왜 Apple Server Notification(ASN)을 도입했을까?

기존에는 앱에서 유저가 접속할 때마다 Apple receipt를 백엔드로 전송해서 유효성을 검증했는데요,
이 방식은 실시간성이 떨어지고, 사용자가 앱을 켜지 않으면 구독 상태가 반영되지 않는 문제가 있었습니다.

이를 해결하고자 애플에서 제공하는 실시간 알림 시스템인 ASN을 도입하게 되었습니다.


인프라 구성: Apple → API Gateway → SQS → Backend

애플은 ASN을 외부 HTTP(S) 엔드포인트로 전송하므로, 이를 안정적으로 처리하기 위해 아래와 같은 구조를 구성했습니다.

Apple ASN → API Gateway (HTTPS endpoint) → AWS SQS → Backend 서버
  • API Gateway: Apple에서 직접 호출할 수 있는 HTTPS 엔드포인트 제공
  • SQS: Apple의 긴 재시도 정책 대응 (ex: 수시간 간격으로 재시도됨)
  • Backend: SQS에서 메시지를 받아 구독 상태 업데이트 처리

이 구조 덕분에 유실 없이 비동기 안정 처리가 가능했습니다.


ASN 서명 검증: SignedDataVerifier

Apple ASN은 JWT 포맷의 서명된 데이터를 전송합니다. 이를 검증하기 위해 SignedDataVerifier를 사용하여 ASN의 유효성을 검증하고, payload를 디코딩했습니다.

주의할 점:

  • 로컬 테스트에서는 Apple이 메시지를 직접 전송하지 않기 때문에 테스트가 어렵습니다.
  • 실제 환경에서는 Apple에서 직접 HTTPS로 메시지를 보내기 때문에 반드시 정식 도메인/SSL 인증서가 필요합니다.

개발 중 겪었던 주요 고민

1. 테스트 어려움

Apple은 로컬 테스트 환경에서는 ASN을 전송하지 않기 때문에, 초기 테스트에 큰 제약이 있었습니다.
API Gateway + SQS 구조를 미리 만들어두고, Apple 콘솔에 등록해서 테스트해야 했습니다.

2. receipt-data 미포함 이슈

ASN에는 receipt-data가 포함되지 않아, 서버에서 별도로 Apple API를 호출해 구독 상태를 확인해야 합니다.

3. Apple의 재시도 정책 대응

ASN 전송 실패 시 Apple은 몇 시간 단위로 재시도하기 때문에, 안정적인 처리를 위해 SQS와 Dead-Letter Queue(DLQ)도 설정해두었습니다.


구현 포인트 정리

  • verifySignedData()로 ASN 서명 검증
    - Apple에서 보낸 ASN인지 검증하는 함수
  • SQS Lambda consumer에서 메시지 처리
    - SQS 쌓인 메시지를 읽고 백엔드로 전달하는 로직
  • 구독 상태(AUTO_RENEW_DISABLED, RENEWAL, CANCEL, EXPIRE 등)에 따라 DB 상태 업데이트

마무리

iOS ASN 개발은 쉽지 않았지만, 앱에서 유저가 앱을 열지 않아도 실시간으로 구독 상태를 반영할 수 있다는 점에서 큰 효과가 있었습니다.

728x90
반응형
반응형

NestJS + Jest 테스트에서 describe, beforeEach, spyOn을 활용하여 의존성을 주입하고 Mock 데이터를 설정하는 방법

🔍 개요

NestJS에서 Jest를 사용해 테스트 코드를 작성할 때,

  • describe()를 사용해 테스트를 계층적으로 나누고,
  • beforeEach()에서 공통 의존성을 주입하며,
  • jest.spyOn()을 활용해 비즈니스 로직 내부의 특정 메서드를 Mocking하여 원하는 데이터를 설정할 수 있습니다.

이 글에서는 NestJS 서비스의 비즈니스 로직을 단위 테스트하는 과정에서 의존성을 주입하고, Mock 데이터를 설정하는 올바른 방법을 설명합니다.


1️⃣ Jest에서 의존성 주입과 Mock 데이터 설정이 필요한 이유

NestJS에서는 의존성 주입(Dependency Injection, DI)을 사용하여 서비스 간의 의존성을 관리합니다.
하지만 Jest에서 단위 테스트를 작성할 때, 실제 서비스 대신 Mock 데이터를 주입해서 테스트합니다.

이유는 다음과 같습니다:

  1. 독립적인 테스트를 수행하기 위해
    • 실제 데이터베이스나 API 호출 없이 테스트를 실행해야 합니다.
  2. 테스트 실행 속도를 높이기 위해
    • 실제 API나 DB를 호출하면 테스트가 느려지고, 불필요한 비용이 발생합니다.
  3. 예측 가능한 테스트를 위해
    • jest.spyOn()을 사용하여 특정 메소드의 반환값을 제어할 수 있습니다.

2️⃣ NestJS에서 Jest 테스트 환경 설정

📌 1. 기본적인 테스트 코드 구조

다음은 NestJS 서비스에서 Jest를 사용한 기본적인 테스트 코드 구조입니다.
샘플 코드) Android 인앱결제 신호를 받는 비지니스 로직 테스트 코드

import { Test, TestingModule } from '@nestjs/testing';
import { AndroidRtdnCommandHandler } from './android-rtdn-command.handler';
import { IAPValidatorProvider } from '../providers/iap-validator.provider';
import { SubscriptionRepository } from '../subscription.repository';
import { AppConfigService } from 'src/config';

describe('AndroidRtdnCommandHandler', () => {
  let handler: AndroidRtdnCommandHandler;
  let iapValidatorProvider: jest.Mocked<IAPValidatorProvider>;
  let subscriptionRepository: jest.Mocked<SubscriptionRepository>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AndroidRtdnCommandHandler,
        {
          provide: IAPValidatorProvider,
          useValue: { validateGooglePurchase: jest.fn() }, // Mock 설정
        },
        {
          provide: SubscriptionRepository,
          useValue: { findByLatestOrderId: jest.fn() },
        },
      ],
    }).compile();

    handler = module.get<AndroidRtdnCommandHandler>(AndroidRtdnCommandHandler);
    iapValidatorProvider = module.get<IAPValidatorProvider>(IAPValidatorProvider);
    subscriptionRepository = module.get<SubscriptionRepository>(SubscriptionRepository);
  });

  describe('processGooglePlayMessage', () => {
    let validateGooglePurchaseSpy: jest.SpyInstance;
    let findByLatestOrderIdSpy: jest.SpyInstance;

    beforeEach(() => {
      validateGooglePurchaseSpy = jest
        .spyOn(iapValidatorProvider, 'validateGooglePurchase')
        .mockResolvedValue({
          latestOrderId: "test-order-id",
          subscriptionState: 2,
          testPurchase: false,
        });

      findByLatestOrderIdSpy = jest
        .spyOn(subscriptionRepository, 'findByLatestOrderId')
        .mockResolvedValue({
          id: "test-id",
          orderId: "test-order-id",
          state: "active",
        } as unknown as Subscription);
    });

    it('should call handleSubscriptionValidate for subscriptionNotification', async () => {
      const message = {
        subscriptionNotification: {
          notificationType: 1,
          purchaseToken: "test-token",
          subscriptionId: "1month_subscription",
        }
      };

      await handler.processGooglePlayMessage(message);

      expect(validateGooglePurchaseSpy).toHaveBeenCalled();
      expect(findByLatestOrderIdSpy).toHaveBeenCalled();
    });
  });
});

3️⃣ Jest에서 의존성을 주입하는 방법

📌 Test.createTestingModule을 사용해 의존성 주입

Test.createTestingModule()을 사용하면 NestJS의 providers를 Mock 객체로 대체하여 테스트 환경을 구성할 수 있습니다.

예제: beforeEach에서 의존성 주입

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [
      AndroidRtdnCommandHandler,
      {
        provide: IAPValidatorProvider,
        useValue: { validateGooglePurchase: jest.fn() }, // Mock 설정
      },
      {
        provide: SubscriptionRepository,
        useValue: { findByLatestOrderId: jest.fn() },
      },
    ],
  }).compile();

  handler = module.get<AndroidRtdnCommandHandler>(AndroidRtdnCommandHandler);
  iapValidatorProvider = module.get<IAPValidatorProvider>(IAPValidatorProvider);
  subscriptionRepository = module.get<SubscriptionRepository>(SubscriptionRepository);
});

이렇게 설정하면 실제 서비스 대신 Mock 객체가 주입되므로, 테스트가 독립적으로 실행될 수 있습니다.


4️⃣ jest.spyOn()을 사용하여 특정 메소드 Mocking

📌 jest.spyOn()을 사용해 특정 함수의 반환값을 변경

단위 테스트에서는 비즈니스 로직이 내부에서 실행하는 특정 메소드를 Mocking할 필요가 있습니다.

예제: 특정 함수의 반환값을 변경

const validateGooglePurchaseSpy = jest
  .spyOn(iapValidatorProvider, 'validateGooglePurchase')
  .mockResolvedValue({
    latestOrderId: "test-order-id",
    subscriptionState: 2,
    testPurchase: false,
  });

const findByLatestOrderIdSpy = jest
  .spyOn(subscriptionRepository, 'findByLatestOrderId')
  .mockResolvedValue({
    id: "test-id",
    orderId: "test-order-id",
    state: "active",
  } as unknown as Subscription);

이렇게 하면 테스트에서 특정 함수의 반환값을 원하는 값으로 설정할 수 있습니다.


5️⃣ jest.spyOn()을 사용해 함수 호출 여부 검증

테스트 실행 후 특정 함수가 정상적으로 실행되었는지 확인해야 합니다.

📌 expect(...).toHaveBeenCalled()을 사용

it('should call handleSubscriptionValidate for subscriptionNotification', async () => {
  const message = {
    subscriptionNotification: {
      notificationType: 1,
      purchaseToken: "test-token",
      subscriptionId: "1month_subscription",
    }
  };

  await handler.processGooglePlayMessage(message);

  expect(validateGooglePurchaseSpy).toHaveBeenCalled(); // ✅ 함수가 호출되었는지 확인
  expect(findByLatestOrderIdSpy).toHaveBeenCalled();
});

이제 이 테스트는 특정 메소드가 호출되었는지 검증할 수 있습니다.


🚀 결론

✔ Jest에서 의존성을 주입하고 Mock 데이터를 설정하는 Best Practice

  1. Test.createTestingModule()을 사용해 Mock 서비스를 주입
  2. beforeEach()에서 공통적으로 필요한 Mock 설정을 적용
  3. jest.spyOn()을 사용해 내부 메소드의 반환값을 설정
  4. expect(...).toHaveBeenCalled()을 사용해 함수 호출 여부를 검증
  5. 중복된 Mock 설정을 beforeEach()에서 한 번만 설정하여 유지보수를 쉽게 만듦

💡 이렇게 하면?

  • 독립적인 단위 테스트 가능
  • 외부 API나 DB 호출 없이 빠른 테스트 실행
  • 비즈니스 로직이 의도한 대로 동작하는지 검증 가능
728x90
반응형

'NestJS' 카테고리의 다른 글

[Nest.js] PIPE  (0) 2023.08.27
[Nest.js] TypeORM 이용한 CRUD  (0) 2023.08.22
[Nest.js] 기본 파일 구조 및 요청 흐름  (2) 2023.08.20
[Nest.js] Quick Start!  (0) 2023.08.15
반응형

 


https://www.credly.com/badges/1eb9a040-948b-4327-ac64-521b1bdbd9c6/public_url

 

AWS Certified Solutions Architect – Associate was issued by Amazon Web Services Training and Certification to gyucheol Kim.

Earners of this certification have a comprehensive understanding of AWS services and technologies. They demonstrated the ability to build secure and robust solutions using architectural design principles based on customer requirements. Badge owners are abl

www.credly.com

 

2024 올해 목표 중에 하나였던 AWS SSA 자격증 취득에 가까스로 성공했다~! 야호~ㅎㅎㅎ
올해 가기 전에 합격해서 참 다행이다,,

현재 회사에서도 AWS 서비스를 많이 사용하고 있어서 조금 더 효율적으로 사용하고 싶은 마음에 자격증 취득 목표를 가졌었는데 성공해서 너무 뿌듯하다!!
시험 합격을 위해서는 문제를 많이 풀어보는 게 정답인 것 같다 물론 개념 정리는 계속하면서! 

아래는 내가 도움 받은 사이트들입니다. 선배님들 감사합니다 ㅎㅎ

[시험에 도움 받은 사이트]
https://julie-tech.tistory.com/128

 

AWS CAA - 덤프 사이트 및 오답노트 공유, 합격후기

오늘은 AWS CAA 자격증 준비과정의 마지막 글이 될 시험 합격후기를 쓰려고 한다. 결국 많은 사람들이 말하지만, "덤프를 열심히 풀고 가세요!"가 한 줄 요약이 될 것 같다! 필자는 2022년 8월 30일부

julie-tech.tistory.com

https://blog.naver.com/PostList.naver?blogId=gam_jaong&categoryNo=18&from=postList&parentCategoryNo=0

 

일본사는 감자옹 : 네이버 블로그

나는 말하는 왜노자 포테이토

blog.naver.com

 

728x90
반응형

'AWS' 카테고리의 다른 글

AWS ECS 이용한 서비스 배포  (0) 2024.04.28
S3 객체 대량 삭제 시 복구 방법  (2) 2023.11.03
AWS를 이용한 SSL 적용하기  (0) 2023.01.08
반응형

배포하실 서비스가 Dockerfile에 준비되어 있다는 전제에서 설명드리겠습니다.

 

순서

  1. ECR 레포 docker 이미지 push
  2. ECS 태스크 정의
  3. ECS 클러스터 생성
  4. 클러스터 서비스 생성

 

ECS 이란?

Amazon Elastic Container Service(Amazon ECS)는 완전관리형 컨테이너 오케스트레이션 서비스입니다. 기본적으로 Amazon Route 53, Secrets Manager, AWS Identity and Access Management(IAM), Amazon CloudWatch 등의 다른 서비스와 통합을 통해 컨테이너 배포 및 확장을 위한 익숙한 환경을 제공할 수 있습니다. 다른 AWS 서비스와의 신속한 통합을 통해 ECS에 새로운 기능을 추가할 수도 있습니다. ECS를 통해 애플리케이션에서 Amazon EC2  AWS Fargate를 스팟 및 온디맨드 요금 옵션과 조합하여 유연하게 사용할 수도 있습니다.


ECR 이란?

AWS ECR (Amazon Elastic Container Registry)는 Amazon Web Services가 제공하는 Docker 컨테이너 이미지를 저장하고 관리할 수 있는 서비스입니다. 이 서비스를 사용하여 개발자는 Docker 이미지를 안전하게 업로드, 저장, 관리 및 배포할 수 있습니다. ECR은 AWS 클라우드의 다른 서비스와 통합되어 컨테이너화된 애플리케이션의 빌드, 저장 및 배포 프로세스를 간소화합니다.

 

1. ECR 레포 docker 이미지 push

1. AWS ECR에 접속해서 원하는 레포 이름으로 레포지토리를 생성합니다.

2. 레포를 생성했다면 푸시 명령 보기를 클릭해서 배포하려는 프로젝트 파일에서 순차적으로 명령어를 실행시켜서 푸시를 진행합니다.(푸시 명령 1~4번 실행)

주의할 점, 배포하는 ECS가 Linux이기 때문에 Linux 환경에서 실행되는 docker 이미지를 push 해야 합니다.
추후에 ECS 서비스를 생성할 때 계속 서킷 브레이커가 발생한다면 docker 이미지를 먼저 의심해 보시면 됩니다.

(저 같은 경우 docker 이미지가 제 MAC에서는 잘 동작해서 ECR에 올렸는데 ECS는 Linux 계열이라서 계속적으로 ECS 서비스 생성에 문제가 발생해서 오랜 삽질을 했습니다,, 저 같은 분들이 없기를,,)

 

 

 

2. ECS 태스크 정의

  • 태스크 정의 패밀리 : 원하시는 이름 작성
  • 시작 유형 : AWS Fargate
  • OS, 아키텍처, 네트워크 모드 : Linux/X86_64
  • 태스크 크기 : 1 vCPU, 3GB
  • 태스크 역할 : ecsTaskExecutionRole
  • 태스크 실행 역할 : ecsTaskExecutionRole

 

컨테이너

  • 이미지 URI : ECR 이미지 URI (주의, ECR 레포 URI 아닙니다.  아래 이미지 참고)
  • 컨테이너 포트 : 컨테이너에서 사용하는 포트 작성
  • 환경변수 파일 추가 : S3에 환경 변수 파일 업로드해서 객체 ARN 값 작성(옵션)
  • 로깅을 선택하면 ecsTaskExecutionRole 값에 CloudWatchFullAccess 권한 추가 필요(옵션, 아래 이미지 참고)

나머지 설정은 기본 설정으로 진행

 

 

ECR 레포 안에 이미지 URI 복사
ecsTaskExecutionRole 권한에 CloudWatchFullAccess 권한 추가

3. ECS 클러스터 생성

  • 클러스터 이름 작성
  • AWS Fargate 선택

나머지 설정은 기본 설정값으로 진행

 

 

4. 클러스터 서비스 생성

환경
- 컴퓨팅 옵션 : 시작 유형

- 시작 유형 : FARGATE
배포 구성

- 애플리케이션 유형 : 서비스

- 패밀리 : 앞에서 정의한 태스크 선택
네트워킹
- 사용하는 VPC 있으면 선택

로드 벨런싱

- 로드 벨런서 연결

- 기존 대상 그룹을 연결할 때는 대상 유형을 “IP 주소 선택해서 생성한 대상 그룹 연결 가능(Fargate 연결하는 경우)

 

 

ECS 서비스 상태가 활성이면 배포 성공!

 

서비스에 들어가면 상태 및 지표와 연결된 로드 벨런서 확인이 가능하고

테스크에 들어가면 태스크 구성(IP등)과 로그를 확인할 수 있습니다.

728x90
반응형

'AWS' 카테고리의 다른 글

AWS SAA-CO3 Certification 취득  (0) 2024.12.08
S3 객체 대량 삭제 시 복구 방법  (2) 2023.11.03
AWS를 이용한 SSL 적용하기  (0) 2023.01.08
반응형

 

핵심 정리

24장 클로저

클로저는 자바스크립트 고유의 개념이 아닙니다. 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성입니다.

클러저는 자바스크립트 고유의 개념이 아니므로 클로저의 정의가 ECMAScript 사양에 등장하지 않습니다.

MDN에서는 클로저에 대해 다음과 같이 정의합니다.

"클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다."

 

1. 렉시컬 스코프

자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정합니다. 

이를 렉시컬 스코프(정적 스코프)라 합니다.

렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정됩니다. 이것이 바로 렉시컬 스코프입니다.

 

2. 함수 객체의 내부 슬롯[[Environment]]

렉시컬 스코프가 가능하려면 함수는 자신이 호출되는 환경과는 상관없이 자신이 정의된 환경, 즉 상위 스코프를 기억해야 한다. 이를 위해 함수는 자신의 내부 슬롯[[Environmnet]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장합니다.

 

3. 클로저와 렉시컬 환경

const x = 1;

function outer() {
 const x = 10;
 const inner = function() { console.log(x); };
 return inner;
}

// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer();
innerFunc(); // 10

실행 결과는 outer 함수의 지역 변수 x의 값인 10입니다. 이미 생명 주기가 종료되어 실행 컨텍스트 스택에서 제거된 outer 함수의 지역 변수 x가 다시 부활이라도 한 듯이 동작하고 있습니다. 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있습니다. 이러한 중첩 함수를 클로저라고 부릅니다.

 

하나씩 코드를 살펴보면

outer 함수가 평가되어 함수 객체를 생성할 때 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 렉시컬 환경을 outer 함수 객체의 [[Environment]] 내부 슬롯에 상위 스코프로서 저장합니다.

outer 함수를 호출하면 outer 함수의 렉시컬 환경이 생성되고 앞서 outer 함수 객체의 [[Environment]] 내부 슬롯에 저장된 전역 렉시컬 환경을 outer 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 할당 합니다. 그리고 중첩 함수 inner가 평가됩니다.

이때 중첩 함수 inner는 자신의 [[Environment]] 내부 슬롯에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 outer 함수의 렉시컬 환경을 상위 스프로서 저장합니다.

중첩 함수 inner는 외부 함수 outer보다 더 오래 생존했습니다. 이때 외부 함수보다 더 오래 생존한 중첩 함수는 외부 함수의 생존 여부와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억합니다. 이처럼 중첩 함수 inner의 내부에서는 상위 스코프를 참조할 수 있으므로 상위 스코프의 식별자를 참조할 수 있고 식별자의 값을 변경할 수도 있습니다.

 

자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저입니다. 하지만 일반적으로 모든 함수를 클로저라고 하지는 않습니다. 상위 스코프의 어떤 식별자도 참조하지 않는 경우 대부분의 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않습니다.

 

클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수(free variable)라고 부릅니다.

 

4. 클로저의 활용

클로저는 상태(state)를 안전하게 변경하고 유지하기 위해 사용합니다. 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용합니다.

// 카운트 상태 변경 함수
const increase = (function (){
 // 카운트 상태 변수
 let num = 0;
 
 // 클로저
 return function() {
  return ++num;
 };
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

 

위 코드가 실행되면 즉시 실행 함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase 변수에 할당됩니다. increase 변수에 할당된 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저입니다.

즉시 실행 함수는 호출된 이후 소멸되지만 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출됩니다. 이때 즉시 실행 함수가 반환한 클로저는 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경으 기억하고 따라서 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경할 수 있습니다.

이처럼 클로저는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용합니다.

728x90
반응형

+ Recent posts