Lambda 애플리케이션 설계하기
Lambda 애플리케이션 설계하기 (Designing Lambda applications)
잘 설계된 이벤트 기반 애플리케이션은 AWS 서비스와 커스텀 코드를 조합해서 요청과 데이터를 처리하고 관리해요. 이 장에서는 애플리케이션 설계에서 Lambda에 특화된 주제를 다룹니다. 바쁜 프로덕션 시스템용 애플리케이션을 설계할 때 서버리스 아키텍트가 고려해야 할 중요한 사항이 많답니다.
소프트웨어 개발과 분산 시스템에 적용되는 모범 사례의 상당수가 서버리스 애플리케이션 개발에도 그대로 적용돼요. 궁극적인 목표는 아래 같은 워크로드를 만드는 거예요.
- 신뢰성(Reliable) — 최종 사용자에게 높은 수준의 가용성을 제공해요. AWS 서버리스 서비스는 장애도 설계에 반영돼 있어서 신뢰할 수 있어요.
- 내구성(Durable) — 워크로드의 내구성 요구를 충족하는 스토리지 옵션을 제공해요.
- 보안(Secure) — 모범 사례를 따르고 제공되는 도구로 워크로드 접근을 보호하고 폭발 반경(blast radius)을 제한해요.
- 성능(Performant) — 컴퓨팅 리소스를 효율적으로 사용하고 최종 사용자의 성능 요구를 충족해요.
- 비용 효율(Cost-efficient) — 불필요한 비용을 피하고, 과도한 지출 없이 확장할 수 있으며, 큰 오버헤드 없이 폐기할 수 있는 아키텍처를 설계해요.
아래 설계 원칙들이 이런 목표를 달성하는 데 도움을 줘요. 모든 원칙이 모든 아키텍처에 적용되는 건 아니지만, 전반적인 아키텍처 결정을 내릴 때 지침이 돼요.
Topics
- 커스텀 코드 대신 서비스 사용하기
- Lambda 추상화 수준 이해하기
- 함수에 무상태성(statelessness) 구현하기
- 결합도 최소화하기
- 배치 대신 온디맨드 데이터를 위한 설계
- 복잡한 워크플로를 위한 오케스트레이션 옵션 선택
- 멱등성(idempotency) 구현하기
- 할당량(quota) 관리에 여러 AWS 계정 사용하기
본문
커스텀 코드 대신 서비스 사용하기
서버리스 애플리케이션은 대개 여러 AWS 서비스로 구성되고, 여기에 Lambda 함수에서 실행되는 커스텀 코드가 통합돼요. Lambda는 대부분의 AWS 서비스와 통합될 수 있지만, 서버리스 애플리케이션에서 가장 흔히 쓰이는 서비스는 다음과 같아요.
| 카테고리 | AWS 서비스 |
|---|---|
| 컴퓨팅 | AWS Lambda |
| 데이터 스토리지 | Amazon S3, Amazon DynamoDB, Amazon RDS |
| API | Amazon API Gateway |
| 애플리케이션 통합 | Amazon EventBridge, Amazon SNS, Amazon SQS |
| 오케스트레이션 | Lambda Durable Functions, AWS Step Functions |
| 스트리밍 데이터·분석 | Amazon Data Firehose |
참고 DynamoDB, Amazon S3를 포함한 많은 서버리스 서비스는 복제와 여러 리전 지원을 제공해요. Lambda 함수는 배포 파이프라인의 일부로 여러 리전에 배포할 수 있고, API Gateway로 이 구성을 지원하도록 설정할 수 있어요. 이를 어떻게 달성하는지 보여주는 예시 아키텍처를 참고하세요.
분산 아키텍처에는 직접 만들 수도, AWS 서비스를 써서 구현할 수도 있는 검증된 공통 패턴이 많아요. 대부분의 고객에게 이런 패턴을 처음부터 개발하는 데 시간을 투자하는 건 상업적 가치가 낮아요. 애플리케이션에서 이런 패턴 중 하나가 필요해지면 해당 AWS 서비스를 사용하세요.
| 패턴 | AWS 서비스 |
|---|---|
| 큐(Queue) | Amazon SQS |
| 이벤트 버스(Event bus) | Amazon EventBridge |
| 발행/구독(fan-out) | Amazon SNS |
| 오케스트레이션 | Lambda Durable Functions, AWS Step Functions |
| API | Amazon API Gateway |
| 이벤트 스트림 | Amazon Kinesis |
이 서비스들은 Lambda와 통합되도록 설계됐고, 인프라를 코드로 관리(IaC)해서 리소스를 만들고 폐기할 수 있어요. AWS SDK를 통해서도 이 서비스들을 쓸 수 있어서, 애플리케이션이나 서버를 설치·구성할 필요가 없어요. Lambda 함수 안의 코드로 이 서비스들을 능숙하게 다루는 일은 잘 설계된 서버리스 애플리케이션을 만드는 중요한 단계예요.
Lambda 추상화 수준 이해하기
Lambda 서비스는 여러분의 함수를 실행하는 기본 OS, 하이퍼바이저, 하드웨어에 대한 접근을 제한해요. 서비스는 기능을 추가하고 비용을 줄이며 성능을 높이기 위해 인프라를 계속 개선하고 바꿔요. 따라서 코드는 Lambda가 어떻게 아키텍처링되는지 알지 못한다고 가정하고, 어떤 하드웨어 선호도(hardware affinity)도 가정하지 말아야 해요.
마찬가지로 Lambda가 다른 서비스와 맺는 통합은 AWS가 관리하고, 노출되는 구성 옵션은 몇 가지뿐이에요. 예를 들어 API Gateway와 Lambda가 상호작용할 때 로드 밸런싱이라는 개념이 없는데, 전적으로 서비스가 관리하기 때문이에요. 또 어느 시점에 함수를 호출할 때 서비스가 어떤 가용 영역(Availability Zone)을 쓰는지, 실행 환경 수를 언제 늘리거나 줄일지 Lambda가 어떻게 결정하는지 직접 제어할 수 없어요.
이런 추상화 덕분에 애플리케이션의 통합 측면, 데이터 흐름, 그리고 워크로드가 최종 사용자에게 가치를 주는 비즈니스 로직에 집중할 수 있어요. 서비스가 기본 메커니즘을 관리하게 두면 유지할 커스텀 코드가 줄어들어서 애플리케이션을 더 빨리 개발할 수 있죠.
함수에 무상태성(statelessness) 구현하기
표준 Lambda 함수에서는 환경이 단일 호출에만 존재한다고 가정해야 해요. 함수는 처음 시작할 때 필요한 상태를 초기화해야 하죠. 예를 들어 함수가 DynamoDB 테이블에서 데이터를 가져와야 한다면, 종료하기 전에 영구 데이터 변경 사항을 Amazon S3, DynamoDB, Amazon SQS 같은 내구성 있는 저장소에 커밋해야 해요. 기존 데이터 구조나 임시 파일, 여러 호출이 관리하는 내부 상태에 의존해서는 안 돼요.
데이터베이스 연결과 라이브러리를 초기화하거나 상태를 로드하려면 정적 초기화(static initialization)를 활용할 수 있어요. 실행 환경은 가능하면 재사용되어 성능을 높이기 때문에, 이런 리소스 초기화에 걸리는 시간을 여러 호출에 걸쳐 분산(amortize)할 수 있죠. 다만 함수 안에서 쓰는 변수나 데이터를 이 전역 스코프에 저장해서는 안 돼요.
결합도 최소화하기
대부분의 아키텍처는 더 적고 큰 함수보다 더 많고 짧은 함수를 선호해야 해요. 각 함수의 목적은 전달받은 이벤트를 처리하는 것이어야 하고, 전체 워크플로나 트랜잭션 규모를 알거나 기대해서는 안 돼요. 이렇게 하면 함수가 이벤트의 출처에 무관(agnostic)해지고 다른 서비스와의 결합이 최소화돼요.
자주 바뀌지 않는 전역 스코프 상수는 환경 변수로 구현해서 배포 없이도 업데이트할 수 있게 하는 게 좋아요. 비밀값이나 민감한 정보는 AWS Systems Manager Parameter Store나 AWS Secrets Manager에 저장하고 함수가 로드해야 해요. 이 리소스들은 계정별로 다르기 때문에 여러 계정에 걸쳐 빌드 파이프라인을 만들 수 있어요. 파이프라인은 환경별로 알맞은 비밀값을 로드하되, 개발자에게 노출하거나 코드 변경을 요구하지 않아요.
배치 대신 온디맨드 데이터를 위한 설계
전통적인 시스템 중에는 주기적으로 실행되면서 시간에 따라 쌓인 트랜잭션 배치를 처리하도록 설계된 것들이 많아요. 예를 들어 은행 애플리케이션이 매시간 실행돼 ATM 트랜잭션을 중앙 원장으로 처리하는 경우죠. Lambda 기반 애플리케이션에서는 커스텀 처리가 모든 이벤트에 의해 트리거되어야 해요. 그래야 서비스가 필요에 따라 동시성을 확장해서 트랜잭션을 거의 실시간으로 처리할 수 있거든요.
표준 Lambda 함수의 실행 시간은 15분으로 제한되지만, Durable Functions는 최대 1년까지 실행될 수 있어서 더 오래 걸리는 처리에도 적합해요. 그래도 가능하면 배치 처리보다 이벤트 주도 처리를 선호하는 게 좋아요.
서버리스 애플리케이션에서 예약 표현식(scheduled expressions)으로 cron 작업을 돌릴 수 있지만, 드물게 또는 최후의 수단으로만 쓰는 게 좋아요. 배치를 처리하는 모든 예약 작업에는 트랜잭션 규모가 Lambda 15분 제한 안에서 처리할 수 있는 범위를 넘어설 가능성이 있어요. 외부 시스템의 제약 때문에 어쩔 수 없이 스케줄러를 써야 한다면, 일반적으로 가장 합리적인 짧은 주기로 예약하는 게 좋아요.
예를 들어 배치 프로세스가 Lambda 함수를 트리거해 새 Amazon S3 객체 목록을 가져오는 건 좋은 사례가 아니에요. 배치 사이에 서비스가 받는 새 객체가 15분짜리 Lambda 함수로 처리할 수 있는 양보다 많아질 수 있기 때문이에요.
Amazon S3 대신, 새 객체가 버킷에 들어올 때마다 Lambda 함수를 호출하게 하는 게 좋아요. 이 방식이 훨씬 확장성이 좋고 거의 실시간으로 동작해요.
복잡한 워크플로를 위한 오케스트레이션 옵션 선택하기
분기 로직, 다양한 실패 모델, 재시도 로직이 포함된 워크플로는 보통 전체 실행 상태를 추적하는 오케스트레이터를 사용해요. 표준 Lambda 함수에 임시방편식(ad-hoc) 오케스트레이션을 만들지 마세요. 그러면 결합도가 높아지고, 라우팅 코드가 복잡해지며, 자동 상태 복구가 없어져요.
대신 목적에 맞게 만들어진 이 오케스트레이션 옵션 중 하나를 사용하세요.
- Lambda Durable Functions — 표준 프로그래밍 언어를 쓰는 애플리케이션 중심 오케스트레이션으로, 자동 체크포인팅, 내장 재시도, 실행 복구를 제공해요. 워크플로 로직을 비즈니스 로직과 함께 Lambda 안의 코드로 두고 싶은 개발자에게 이상적이에요.
- AWS Step Functions — 220개 이상의 AWS 서비스와 네이티브 통합되는 시각적 워크플로 오케스트레이션. 다중 서비스 조정, 유지보수 없는 인프라, 시각적 워크플로 설계에 이상적이에요.
이 두 옵션 사이의 선택에 대한 안내는 Durable functions or Step Functions를 참고하세요.
Step Functions에서는 상태 머신을 사용해 오케스트레이션을 관리해요. 오류 처리, 라우팅, 분기 로직을 코드에서 빼내고, 대신 JSON으로 선언한 상태 머신으로 대체하죠. 워크플로가 더 견고하고 관찰 가능해지는 것 외에도, 워크플로에 버전을 추가하고 상태 머신을 코드 저장소에 추가할 수 있는 코드화된 리소스로 만들 수 있어요.
Lambda 함수의 단순한 워크플로가 시간이 지나면서 복잡해지는 경우가 흔해요. 프로덕션 서버리스 애플리케이션을 운영할 때는 이런 변화가 일어나고 있음을 알아채고, 그 로직을 상태 머신이나 Durable Function으로 옮기는 게 중요해요.
멱등성(idempotency) 구현하기
Lambda를 포함한 AWS 서버리스 서비스는 장애 허용(fault-tolerant)이 되도록 설계돼 있고 실패를 처리하도록 만들어졌어요. 예를 들어 서비스가 Lambda 함수를 호출했는데 서비스 중단이 있으면, Lambda는 다른 가용 영역에서 함수를 호출해요. 함수가 오류를 던지면 Lambda는 호출을 재시도하죠.
같은 이벤트가 한 번 이상 수신될 수 있으므로 함수는 멱등적(idempotent)으로 설계해야 해요. 즉, 같은 이벤트를 여러 번 받아도 처음 수신했을 때의 결과를 넘어 결과가 바뀌지 않아야 한다는 뜻이에요.
Lambda 함수에서 멱등성은 DynamoDB 테이블로 최근 처리된 식별자를 추적해서 해당 트랜잭션이 이미 처리됐는지 판단하는 방식으로 구현할 수 있어요. 이 DynamoDB 테이블은 보통 Time To Live(TTL) 값을 구현해 항목을 만료시켜 저장 공간 사용을 제한해요.
할당량(quota) 관리에 여러 AWS 계정 사용하기
AWS의 많은 서비스 할당량(service quotas)은 계정 수준에서 설정돼요. 그래서 워크로드를 추가할수록 한도를 빠르게 소진할 수 있어요.
이 문제를 푸는 효과적인 방법은 여러 AWS 계정을 사용해서 각 워크로드를 자기 계정에 전용하는 거예요. 그러면 할당량이 다른 워크로드나 비프로덕션 리소스와 공유되지 않아요.
또한 AWS Organizations를 쓰면 이들 계정의 결제, 규정 준수, 보안을 중앙에서 관리할 수 있어요. 계정 그룹에 정책을 연결해서 커스텀 스크립트나 수동 프로세스를 피할 수 있죠.
흔한 접근법 중 하나는 각 개발자에게 AWS 계정을 하나씩 주고, 베타 배포 단계와 프로덕션에는 별도 계정을 사용하는 거예요. 이 모델에서는 각 개발자가 자기 계정만의 한도를 갖기 때문에, 개발자의 사용이 프로덕션 환경에 영향을 주지 않아요. 개발자들은 자기 계정의 라이브 클라우드 리소스에 대해 개발 머신에서 Lambda 함수를 로컬로 테스트할 수도 있어요.