AWS 기술 블로그
Amazon Q Developer CLI로 Lambda를 Rust로 변환하고 성능 향상하기
Amazon Q Developer에서 2025년 4월 출시한 Amazon Q Developer CLI는 Amazon Bedrock으로 구동되며 강력한 기능을 가지고 있습니다. CLI 에이전트는 컴파일러, 패키지 관리자 및 AWS CLI 등 시스템에 설치된 도구를 사용하며, 멀티 턴 대화용 에이전트와 동적인 양방향 대화를 할 수 있는 다중 턴 대화를 지원합니다. 또한, 한국어를 포함한 다양한 언어를 지원하고 있습니다. Amazon Q Developer CLI는 바이브 코딩(Vibe coding)을 가능하게 하고 단순히 애플리케이션 코드 작성 뿐 아니라, 인프라 배포, 운영시 문제 진단 등을 위해 다목적으로 사용할 수 있는 에이전트입니다. 웹 프레임워크를 활용하여 EC2 위에 올라갈 애플리케이션 혹은 ECS, EKS 등에 올라갈 컨테이너화된 애플리케이션을 작성할 때도 사용할 수 있지만, 서버리스 환경에 올라가는 함수를 작성/배포하는 것에도 매우 용이하게 활용할 수 있습니다. 특히 기존 코드의 분석을 통한 개선이나, 다른 언어로의 변환 작업도 지원합니다.
AWS Lambda는 널리 알려진 서버리스 컴퓨팅 플랫폼으로, Lambda의 리소스(실행 시간과 메모리) 사용량이 적을수록 비용이 절감되는 구조입니다.즉, 더 리소스 효율적인 코드를 작성하면 동일한 작업을 더 낮은 비용으로 처리할 수 있습니다.
본 글에서는 서버리스 컴퓨팅 플랫폼의 특징을 잘 활용할 수 있도록 기존 Lambda 코드를 고성능 언어인 Rust로 변환하는 과정을 안내합니다. Rust에 대한 이해도가 높지 않더라도 효율적인 코드를 작성하도록 돕는 Amazon Q Developer CLI를 활용하는 방법을 소개합니다.
Rust를 사용할 때의 장점
Rust는 시스템 엔지니어링에서 널리 사용되고 있는 프로그래밍 언어로 저수준 코드를 쉽고 안전하게 작성할 수 있습니다. 메모리 안전성, 동시성 및 비동기 처리의 편리한 구현 등 다양한 장점들이 있지만 특히 높은 런타임 성능이 Lambda에서 사용할 때의 가장 큰 장점입니다. Rust는 비용 없는 추상화(Zero Cost Abstractions)를 원칙으로 하여 이를 통해 전역으로 성능을 떨어뜨리는 추상화가 존재하지 않습니다. 또한 컴파일 과정에서 코드를 기계어로 번역하고 최적화하는 작업을 하기 때문에 일반적으로 타 언어에 비해 높은 런타임 성능을 가지고 있습니다.
Lambda는 실행 시간과 메모리를 기준으로 비용이 발생하며, Rust를 Lambda에서 실행하면 더 적은 실행 시간과 메모리로 빠르게 실행되어 비용 절감 효과를 기대할 수 있습니다. 즉, Lambda에서 더 좋은 성능은 더 적은 비용으로 이어지며 Rust는 이를 달성하기 위해 적합한 언어입니다. 그러나 Rust를 사용했다고 갑자기 마법처럼 성능이 좋아지는 일은 쉽게 발생하지 않습니다. Rust의 성능을 충분히 활용하려면, 언어의 특성을 이해하고 최적화된 코드를 작성하는 노력이 필요합니다. 만약 Rust로의 전환이 큰 성능 향상을 가져오지 않는다면, 기존에 익숙한 언어를 계속 사용하는 것이 유지보수 측면에서 더 효율적일 수 있습니다.
서술한 장점들로 인해 AWS에서는 여러 핵심 서비스에 Rust를 도입하고 있습니다. Lambda의 경우, 이를 뒷받침하는 가상화 기술인 Firecracker가 Rust로 작성되었으며 EC2의 Nitro Enclaves와 같은 컴포넌트들도 Rust로 작성되어 있습니다. 또한 2023년 Re:Invent에서 발표한 Amazon ElastiCache Serverless의 프록시 레이어도 Rust로 작성되어 있습니다. 더 자세한 내용은 AWS Rust 팀은 러스트의 성공을 위해 어떻게 기여하는가를 읽어보시는 것을 추천드립니다. 이 뿐 아니라 애플리케이션 현대화를 위한 AWS Transform을 통해 .NET과 Mainframe 등 애플리케이션의 변환을 돕는 서비스가 존재합니다.
Rust 도입의 실질적 어려움
이런 장점에도 불구하고 Rust로 코드를 작성하는데 어려움을 겪는 경우가 많습니다. 이는 Rust가 상대적으로 진입 장벽이 높은 언어이기 때문입니다. 개인마다 느끼는 어려움에는 차이가 있지만 몇 가지 이유들을 간단히 소개하면 다음과 같습니다 :
- 고수준 언어(Python, Javascript)에 익숙하다면 저수준 언어 작성에 어려움을 겪을 수 있음
- 작은 실수, 타이포(typo) 오류가 컴파일 오류로 이어짐
- 타입 시스템 중심의 개발 철학으로 인한 타입 모델링의 중요성을 이해해야 함
- 소유권/빌림(borrow) 모델, 라이프타임(lifetime), 매크로 등 익숙하지 않은 언어 철학
- 타 언어 사용자에 비해 적은 Rust 사용자
꼭 Rust로 모든 코드를 변환해야 할까?
따라서 어려움과 진입 장벽으로 인해 모든 코드를 Rust로 전환하기 보다는 코드의 일부분만을 Rust로 전환해서 성능적 이점을 얻는 경우도 있습니다. 2023년 Re:Invent에서 발표한 “Rustifying” serverless: Boost AWS Lambda performance with Rust 에서는 PyO3와 Maturin을 활용하여 Lambda에서 사용되는 Python 코드의 일부분만을 Rust로 전환하는 방법과 성능 향상 사례를 다뤘습니다.. Python 코드를 Rust로 전환한 후 벤치마크 결과를 보면, Warm & Cold Start 모두에서 성능이 크게 개선되었고, 비용도 감소하는 것을 확인할 수 있었습니다. 자세한 결과는 해당 세션을 정리한 글을 참고하시길 바랍니다.
핵심 로직을 Rust로 작성하되, 여러 언어를 래핑하는경우도 있습니다. Valkey OSS에서 개발하고 있는 Valkey-GLIDE의 경우 핵심 로직은 Rust로 작성되었고 Java, Node.js, Python, Golang와 같은 언어에서 사용할 수 있도록 래핑되어 있습니다. 이렇게 작성하면 Rust로 작성된 핵심 로직을 통해 뛰어난 런타임 성능을 확보하면서 여러 언어로 작성된 마이크로서비스들에 라이브러리를 쉽게 도입할 수 있습니다.
이러한 어려움을 어떻게 해결할 수 있을까?
가장 좋은 방법은 Rust의 언어 철학을 이해하고 작은 코드 조각부터 차근차근 Rust로 변경해보며 Rust에 대한 이해도를 높이는 것이겠지만, 현실 세계에서 일반적으로 100~200줄 미만인 Lambda 코드를 Rust로 전환하기 위해 이와 같은 노력을 들이는 것은 오히려 비효율적일 수 있습니다. 이 뿐 아니라 조직 저반이 Rust에 익숙치 않을 경우, Rust로 작성된 Lambda 코드를 유지보수하는데 어려움을 겪고 다시 기존 언어로 돌아오며 불필요한 리소스 낭비가 일어날 수도 있습니다. 만약 조직 내에 Rust에 능숙한 프로그래머가 있다고 해도, 모든 리소스를 코드 변환에 쏟는 것은 비효율적입니다.
다행히 최근에는 생성형 AI의 발전으로 인해 Q Developer와 같은 도구를 활용해 코드를 손쉽게 변환할 수 있게 되었습니다.
어떤 것을 변환해볼까요?
AWS Korea 내에는 aws-whats-new-kr이라는 채널이 존재합니다. 이 채널은 AWS의 최신 소식(영문)을 빠르게 한국어로 기계 번역하여, 조직 내 세일즈 및 테크 직원들이 새로운 정보를 신속하게 파악하고 고객에게 전달할 수 있도록 지원합니다. 해당 채널은 Amazon EventBridge와 Lambda를 기반으로 구현되었으며, 2023년부터는 Amazon Translate를 활용해 자동 번역을 시작했습니다. 현재는 Amazon Bedrock을 활용한 카테고리 분류, 요약, 개인화된 메시지 전달 기능까지 꾸준히 발전해오고 있습니다.
해당 함수는 Node.js를 기반으로 작성되어 있습니다. AWS SDK for JavaScript v3를 사용하여 콜드 스타트 시간을 줄이고, Best practice에 기반하여 코드를 재작성하는 등의 여러 최적화 노력이 반영되어 있습니다. 또한 N개 이상의 소식을 번역/요약/분류할 때에도 비동기 처리를 통해 최대 실행 시간은 10초 미만으로 처리되고 있습니다. 그럼 해당 코드를 Rust로 어떻게 변환했는지와 함께 Q Developer CLI를 통해 어떻게 변환하는 것이 좋았는지 살펴보겠습니다.
Q Developer CLI를 활용하여 변환하기
다음의 7가지 팁을 통해 기존에 작성된 js 코드를 rust로 바꿔나가겠습니다. 다만, 프롬프트에 따른 하기 팁의 결과들은 항상 동일하지 않을 수 있습니다.
Tip 1. 가능하면 구체적이고 명시적인 첫 프롬프트 주기
이미 프롬프트를 잘 작성하는 방법 중 하나로 널리 알려져 있음에도 불구하고, Q Developer CLI를 사용하다보면 의외로 지키기 어려운 팁입니다. Q Developer CLI의 특성상, CLI 명령어를 입력하듯 한 줄의 명령어를 넣고 이에 대한 적절한 응답이 오기를 기대하게 됩니다. 그러나 적어도 처음에는 가능하면 구체적이고 명시적인 프롬프트를 작성하는 것이 필요합니다. 백그라운드 설명부터 시작해서 어떤 작업을 수행하고 싶은지까지 면밀하게 작성해야 모델이 앞으로의 작업 목적을 이해하고 이에 맞는 적절한 응답을 제공할 수 있습니다. 만약 백그라운드에 대한 설명을 강조하거나 추가하기 번거롭다면 마크다운 파일을 하나 만들어 모델이 직접 읽고 분석하도록 하는 것도 좋은 방법입니다.
예를 들어, 다음과 같이 매우 간단한 프롬프트를 줬다고 가정하겠습니다.
index.js를 읽어서 rust로 바꿔줘
해당 프롬프트를 그대로 수행하게 되면 Q Developer는 index.js 파일이 Lambda를 위한 코드인지, 어떤 것을 고려해서 rust로 작성해야 하는지와 같은 것들을 잘 인지하지 못하고 말 그대로 “변환”만 해주게 됩니다. 따라서 프롬프트는 다음처럼 구체적인 것이 좋습니다.
index.js에 있는 코드는 AWS Lambda 함수에서 사용하고 있는 코드야. 해당 코드를 간략하게 설명하면, N시간 전에 들어온 AWS 소식을 RSS 피드로 가져와서 Amazon Bedrock을 통해 번역, 요약, 카테고리 분류를 하고 이를 S3에 저장해 그리고 나서 웹훅을 통해 메신저로 전달하고 있어. index.js에 있는 코드를 Rust로 변환해주되, Lambda에서 실행된다는 점을 고려해서 Rust로 변환해줘.
실제로 구체적인 프롬프트를 주지 않았을 때는 단순 변환만 했으나, 구체적인 프롬프트를 준 후에는 Rust의 패키지 매니저인 카고(Cargo)에 대한 설정과 빌드를 위한 스크립트까지 작성하는 것을 확인할 수 있었습니다.
[그림 1. 단순 프롬프트의 결과]
[그림 2-1, 2-2. 구체적인 프롬프트의 결과]
TIp 2. Crate의 버전을 최신 버전 혹은 적절한 버전으로 높여주세요.
카고는 Rust 빌드 시스템 및 패키지 매니저이며, 카고를 통해 다운받는 코드 패키지를 크레이트(Crate)라고 부릅니다. 첫 번째 팁에서 구체적인 프롬프트를 통해 Cargo.toml이 생성되면 각 의존성에 대한 버전이 기재됩니다. 이때 모델이 생성한 Cargo.toml을 살펴보면 라이브러리의 버전이 매우 낮은 경우가 존재합니다. 특히 Amazon Bedrock의 신규 기능을 사용해야 한다면 가장 최신의 AWS SDK를 사용해야 할 필요가 있습니다.
그러나 Amazon Q Developer CLI를 사용하면 최신 버전을 잘 반영하지 못하는 것을 확인할 수 있습니다. 이는 모델이 학습한 데이터와도 연관이 있을 수 있으며, 따라서 사용할 SDK/라이브러리들의 버전을 한 번씩 체크하고 높여주는 것이 필요합니다. crates.io와 같은 사이트에서 사용할 패키지의 이름을 검색한 후, 적절한 버전을 사용하면 됩니다. 그림 3을 보면 생성된 Cargo.toml에서는 aws-sdk-bedrockruntime이 0.34 버전이었으나 그림 4에서 보는 것처럼 최신 버전은 1.88.0임을 확인할 수 있습니다. 참고로 기존 코드에서 사용하고 있던 Converse의 경우 1.31.0 버전부터 사용이 가능하며 이로 인해 1.31 버전 이하의 버전에서는 컴파일시 에러가 발생합니다.
[그림 3. Q Developer CLI를 통해 생성된 Cargo.toml]
[그림 4. 실제 aws-sdk-bedrockruntime의 최신 버전]
Tip 3 : 참고할 수 있는 예시 코드를 제시해주세요.
두 번째 팁과 이어지는 내용으로, Amazon Bedrock은 매우 빠르게 발전하고 있는 서비스로 Amazon Q Developer CLI로는 이 서비스의 신규 API를 활용하는 코드를 잘 작성하기 어렵습니다. 따라서 참고할 수 있는 예시 코드를 제공하는 것이 필요합니다. 이러한 예시 코드는 AWS SDK for Rust 개발자 가이드에 AWS 서비스별로 제공됩니다. 문서뿐만 아니라 GitHub에도 코드 예제를 제공하고 있기 때문에 이를 그대로 활용할 수 있습니다. GitHub에 있는 코드 예제 혹은 개발자 가이드에 있는 내용을 복사해서 같은 디렉토리에 sample_converse.rs라는 이름으로 예시 코드를 넣습니다. 그 다음으로 이런 프롬프트를 CLI에 입력합니다.
sample_converse.rs는 aws-sdk-bedrockruntime 크레이트를 활용해 converse 메서드로 모델을 호출하고 응답을 가져오는 내용이 담겨 있어. sample_converse.rs를 참고해서 main.rs에 있는 aws-sdk-bedrockruntime를 사용한 호출을 변경해줘.
다음과 같이 코드의 차이를 확인할 수 있습니다.
[코드 1. 기존 생성된 코드]
[코드 2. 예시 코드를 제공한 경우]
Tip 4 : 개선/변경을 요청하기 전에 코드를 분석해달라고 요청하세요.
새 코드를 작성해달라고 하거나, 작성된 코드를 기반으로 개선/변경을 요청해야 하는 경우가 있습니다. 생각의 사슬(Chain-Of-Thought, COT) 프롬프트 기법에 따라 코드를 먼저 분석해달라고 하면 모델은 해당 코드가 어떤 코드인지 충분히 이해하게 됩니다. 이후 이해한 내용을 바탕으로 코드를 더 적절하게 작성/변경할 수 있게 됩니다. 첫 번째 팁과도 이어지는 부분으로 기존 프롬프트 기법 역시 여전히 유용하다는 점을 기억하세요.
Tip 5. 코드를 변경했다면 변경했다고 모델에게 알려주세요.
Q Developer CLI에게 모든 것을 맡길 수 있으면 좋겠지만 간혹 해결하지 못하는 오류가 발생할 수가 있습니다. 이 경우 개발자가 직접 코드를 읽고 확인하여 수정하거나 혹은 추정되는 원인과 함께 수정을 모델에게 요청할 수도 있습니다. 수정 요청 전에 모델이 이미 코드를 읽어둔 상태라면 모델은 컨텍스트에서 이전에 읽거나 수정한 코드를 바탕으로 작업할 수 있습니다. 중간에 사람이 직접 코드를 수정한 내역이 있다면, 모델은 이를 인식하지 못하고 이전 버전의 코드를 기준으로 다시 수정할 수 있습니다. 따라서 코드를 수동으로 변경했다면 변경 사항을 알려 모델이 최신 코드를 반영해 수정하도록 합니다.
Tip 6 : Rust스럽게 작성해달라고 요청하세요.
기존 코드가 JS로 작성된 경우 Rust로 변환하더라도 Rust의 언어적 특성이 잘 반영되지 않을 수 있습니다. 즉, Rust로 재작성되었지만 js를 억지로 Rust로 바꾼듯한 코드가 존재하게 됩니다. 이로 인해 컴파일 시 에러가 발생하고 변경 요청을 해도 에러가 쉽게 해결되지 않을 수 있습니다.
여러 해결책 중 가장 효용이 좋았던 해결책은 “Rust스럽게 코드를 재작성해줘”였습니다. 읽기 어려웠던 코드에서 불필요한 라인들을 정리하며 코드가 전반적으로 명료해졌고 이로 인해 에러를 확인하기 더욱 쉬웠습니다. 실제로 재작성 요청을 한 후 응답의 첫 줄이 “Rust 코드를 더 관용적인(idiomatic) Rust 스타일로 재작성하겠습니다. Rust의 강점을 살리고 더 효율적이고 안전한 코드로 개선하겠습니다.”이었기 때문에 모델이 최대한 언어의 철학과 모범 사례를 기반으로 코드를 재작성하려고 하는 것을 확인할 수 있었습니다.
Tip 7 : 최적화 방법을 물어보고, 이를 통해 개선하세요.
이러한 과정을 거쳐 Lambda에 무사히 배포했다면 했다면 이제 정상적으로 실행되는지 테스트해야 합니다. 그러나 Rust로 작성한 코드가 기대와 달리 더 느리게 동작할 수 있습니다. 실제로 Rust로 변경한 코드를 실행했을 때 약 35초 정도의 실행 시간이 걸렸으며 이는 기존 Node.js로 작성된 코드보다 3~4배 정도 느렸습니다. 이 문제를 해결하기 위해 여러 단계의 최적화 과정을 통해 코드를 최적화하였습니다.
- (사람) 원인 파악을 위해 구간별로 실행 시간을 남겨달라고 모델에게 요청
- (Q Developer CLI) 코드 재작성 및 빌드 후 테스트
- (사람) 느린 구간 확인 : Bedrock 요청하는 부분에서 순차 요청을 했기 때문에 가장 오랜 시간이 걸림. 해당 부분을 개선할 수 있는 방법을 제안해달라고 모델에게 요청
- (참고) Q Developer CLI를 통해 로그를 주고 분석하게 할 수도 있음
- (Q Developer CLI) 최적화 방법을 7가지 정도 제안. 예시는 다음과 같으며 그림 3 참고
- SDK 클라이언트를 함수 외부에서 초기화
- 순차적으로 Bedrock 요청이 가고 있던 것을 비동기적으로 처리
- 순차적으로 Bedrock 요청이 가고 있던 것을 병렬적으로 처리
- Lambda의 메모리 증설
- 등 다양한 방법 제시
- (사람) 이 중 a, b를 선택하여 최적화 요청
- (Q Developer CLI) 성능을 최적화히여 코드를 재작성한 후 재배포
[그림 5. 캡쳐한 Q CLI Developer의 최적화 제안]
개선을 진행하면서 가장 좋았던 점은 개발자가 생각하고 있을법한 방법에 더해 다른 방법들도 추가적으로 제안한다는 점이었습니다. 여러 선택지 중 현재 상황에 맞는 적절한 선택지를 골랐으며, 각 선택지마다 새로 작성된 코드를 읽어 보고 중간 중간 재변경을 요청하며 개선된 코드를 완성해 나갔습니다.
개선 결과
다음은 기존 작성된 Lambda와 Rust로 재작성된 Lambda의 성능 모니터링 지표입니다.
[그림 6. Node.js로 기존 작성된 Lambda의 성능 모니터링 지표]
[그림 7. Rust로 재작성된 Lambda의 성능 모니터링 지표]
실행 시간은 평균적으로 약 35%, 최대 34%까지 단축되었습니다. 또한 Rust는 기존 코드보다 더 낮은 실행 시간과 일관된 성능을 보여주었습니다. 특히 기존 작성된 코드는 Node.js를 활용하는데 V8 엔진을 통해 최적화가 많이 되는 것으로 널리 알려져 있습니다. 그렇기 때문에 Python을 사용하는 경우 더 큰 폭의 성능 개선이 가능할 것으로 예상됩니다.
메모리의 경우, 기존 코드는 거의 100%를 사용하고 있었지만 Rust로 작성된 코드는 41~ 42%를 사용한 것을 확인할 수 있습니다. 또한 CPU 사용량 측면에서도 기존 코드의 최대 값이 500ms, 최소 값이 250ms인 것에 비해 Rust의 최대 값이 220ms이고 최소 값이 100ms인 것을 확인할 수 있었습니다. 따라서 리소스도 Rust로 재작성된 코드가 더 일관적으로 적게 사용하고 있음을 확인할 수 있습니다.
비용의 경우, 약 34% 저렴해졌습니다. (단, 다음의 조건으로 계산되었습니다. 서울 리전 / 초당 1회 요청 / 128MB 메모리 / 각 요청의 기간은 최댓값) 메모리가 더 필요한 상황이라면 Rust로 작성된 코드는 비용최적화가 가능합니다. 실제로 기존 작성된 코드의 메모리를 2배인 256MB로 늘렸다면, 약 67%까지 비용을 절약할 수 있습니다.
결론적으로 Rust로 재작성된 코드는 리소스와 실행 시간 관점 모두에서 큰 폭으로 개선된 모습을 보여줬습니다. 또한 한 가지 흥미로운 점은 Node.js를 활용한 기존 코드가 적정 수준으로 최적화되어 있음에도 불구하고 Rust로 재작성한 코드가 더 적은 리소스 사용률과 더 좋은 성능을 보여줬다는 점입니다. 다만, 이러한 결과가 모든 Lambda 함수에 적용되는 것은 아니며, 처음 재작성한 코드의 성능이 기대에 못 미쳤던 경우도 있었습니다. 즉, 경우에 따라 Rust가 항상 더 나은 성능을 보장하는 것은 아니라는 점을 유의해야 합니다.
마무리
생성형 AI의 발전에 따라 우리는 더 많은 코드를 더 쉽게 작성할 수 있게 되었습니다. 다만 실제 변환 과정을 살펴본 것처럼여전히 개발자의 판단과 개입이 필요한 부분이 많습니다. 최적화된 코드와 향상된 성능의 코드를 작성하기 위해서는 개발자가 이를 판단하고 지침을 내려줘야 합니다. 또한 최신 기술이라면 생성형 AI가 잘 하지 못하는 부분이 존재할 수 있으므로, 예시 코드를 직접 제공하거나 직접 수정해야 효과적인 경우도 있습니다.
그럼에도 불구하고 생성형 AI를 적절히 활용하는 것은 개발자의 생산성을 크게 높여줄 수 있습니다. Rust를 단 하나도 모르는 개발자가 Lambda함수를 Rust 코드로 변환하고 운영하는 것은 쉬운일이 아니겠지만 Rust에 어느 정도 익숙한 개발자라면 변환과 최적화 같은 반복 작업을 생성형 AI에게 충분히 맡길 수 있습니다. 특히 생성형 AI가 단순 작업을 하는 동안 동시에 개발자는 운영 및 개발을 포함한 다른 작업에 리소스를 활용할 수 있다는 점이 큰 장점입니다.
처음 모델을 통해 생성된 코드는 많이 부족했고 실행했을 때 3배나 느렸지만, 개발자와 Amazon Q Developer CLI가 협업하며 다시 30% 이상의 성능 향상을 이뤄낼 수 있었습니다. 여러분도 생성형 AI를 활용한 Lambda 최적화를 통해 비용 절감과 성능 모두를 잡을 수 있기를 바랍니다.