서버리스 API, AWS Lambda로 가는 가장 현실적인 길

사진을 업로드하고 거래하는 소규모 장터 애플리케이션의 서버를 물려받으면서 거대한 도전과 마주했습니다. 초기 아키텍처는 AWS를 사용하고 있었지만, 모든 것이 하나의 EC2 인스턴스 안에 위태롭게 공존하는 형태였습니다. Tomcat, Spring으로 구현된 웹/API 서버, 그리고 데이터베이스인 MySQL까지 말이죠. 이른바 '모놀리식(Monolithic) 지옥'의 전형적인 모습이었습니다. 이대로는 서비스의 미래를 장담할 수 없다는 판단하에, 저는 AWS의 다양한 서비스들을 활용하여 아키텍처를 단계적으로 재구성하는 대장정을 시작하기로 결심했습니다.

이 글은 단순히 특정 AWS 서비스를 나열하는 설명서가 아닙니다. 하나의 거대한 덩어리였던 애플리케이션을 어떻게 분리하고, 왜 그런 결정을 내렸으며, 그 과정에서 어떤 기술적 고민을 했는지에 대한 생생한 기록입니다. 특히 AWS LambdaAPI Gateway를 활용하여 어떻게 Serverless API 환경으로 점진적으로 전환했는지에 대한 현실적인 전략을 공유하고자 합니다. 저와 비슷한 고민을 하고 있는 개발자분들께 실질적인 도움이 되기를 바랍니다.

1단계: 모든 문제의 시작, 모놀리식 아키텍처 분석

처음 마주한 아키텍처는 단순했지만, 그만큼 많은 문제점을 내포하고 있었습니다. AWS t2.medium과 같은 단일 EC2 인스턴스 위에 애플리케이션 서버(Tomcat/Spring)와 데이터베이스(MySQL)가 함께 동작하고 있었습니다. 사용자가 업로드하는 모든 사진 파일은 EC2 인스턴스에 연결된 EBS(Elastic Block Store) 볼륨에 직접 저장되었습니다. 이 구조는 개발 초기에는 빠르고 간편할 수 있지만, 서비스가 조금만 성장해도 곧바로 한계에 부딪힙니다.

무엇이 문제였을까?

  • 단일 장애점 (Single Point of Failure, SPOF): EC2 인스턴스 하나에 모든 기능이 집중되어 있어, 이 인스턴스에 문제가 생기면 서비스 전체가 마비됩니다. 예를 들어, 갑작스러운 트래픽 증가로 웹 서버의 CPU 사용량이 치솟으면, 동일한 자원을 공유하는 MySQL 데이터베이스 성능까지 급격히 저하되는 현상이 발생했습니다. 심지어는 Out of Memory 오류로 Tomcat과 MySQL이 함께 종료되는 아찔한 경험도 했습니다.
  • 확장성의 부재: 서비스의 특정 기능, 예를 들어 이미지 처리 API에만 부하가 몰려도 우리는 API 서버만 독립적으로 확장할 수 없습니다. 불필요하게 데이터베이스와 웹 페이지 렌더링 부분까지 포함된 EC2 인스턴스 전체를 통째로 복제하고 스케일 아웃(Scale-out)해야 합니다. 이는 매우 비효율적이고 비용 낭비가 심합니다.
  • 배포의 어려움과 위험: 아주 작은 코드 수정이라도 전체 애플리케이션을 다시 빌드하고 배포해야 합니다. 이 과정에서 예상치 못한 버그가 발생하면 전체 서비스에 영향을 미치게 되어 배포 과정은 항상 살얼음판을 걷는 기분이었습니다. 기능별로 독립적인 배포가 불가능하여 개발 및 운영 속도가 현저히 저하되었습니다.
  • 상태 저장 서버 (Stateful Server): 가장 시급한 문제였습니다. 사용자가 업로드한 이미지 파일이 EC2 인스턴스의 로컬 스토리지(EBS)에 저장되고 있었습니다. 이는 향후 Auto Scaling을 적용하는 데 치명적인 걸림돌이 됩니다. 오토 스케일링으로 새로운 EC2 인스턴스가 실행되더라도, 기존 인스턴스에 저장된 이미지 파일에는 접근할 수 없기 때문입니다. 모든 인스턴스가 동일한 파일 시스템을 공유해야 한다는 제약이 생기는 것이죠.

개발자의 고백: 솔직히 말해, 처음에는 '이 정도 트래픽에 굳이 구조를 바꿔야 하나?'라는 안일한 생각도 했습니다. 하지만 장애는 항상 예고 없이 찾아옵니다. 새벽에 데이터베이스가 멈췄다는 알람을 받고 부랴부랴 EC2에 접속해 MySQL을 재시작했던 경험은, 아키텍처 개선이 더 이상 미룰 수 없는 최우선 과제임을 깨닫게 해주었습니다.

2단계: 정적 파일 해방, AWS S3로 숨통 트기

가장 먼저 해결해야 할 문제는 단연 이미지 파일 저장 방식이었습니다. EC2 인스턴스를 상태 비저장(Stateless) 서버로 만드는 첫걸음은, 인스턴스 내부에 저장되던 모든 상태(State), 즉 이미지 파일을 외부 스토리지로 분리하는 것이었습니다. 이 문제에 대한 AWS의 대답은 명확했습니다. 바로 S3 (Simple Storage Service)입니다.

왜 S3를 선택해야만 했나?

S3는 단순한 온라인 파일 저장소가 아닙니다. 클라우드 네이티브 애플리케이션을 위한 핵심 구성 요소이며, 다음과 같은 압도적인 장점을 제공합니다.

  • 내구성 및 가용성: AWS S3는 99.999999999%(9가 11개)라는 경이로운 내구성을 보장하도록 설계되었습니다. 이는 사실상 파일이 유실될 확률이 거의 없다는 의미입니다. 또한 여러 가용 영역(Availability Zone)에 데이터를 자동으로 복제하여 높은 가용성을 제공합니다. 더 이상 EBS 볼륨의 스냅샷을 주기적으로 생성하고 관리하는 데 신경 쓸 필요가 없어졌습니다.
  • 무한한 확장성: S3는 저장 용량에 제한이 없습니다. 서비스가 성장하여 수십 테라바이트, 페타바이트의 이미지가 쌓여도 걱정할 필요가 없습니다. EBS 볼륨처럼 용량 증설을 위해 서버를 중단하고 볼륨을 교체하는 번거로운 작업을 할 필요가 없습니다.
  • 비용 효율성: 일반적으로 EBS에 비해 S3의 스토리지 비용은 훨씬 저렴합니다. 특히 자주 액세스하지 않는 데이터는 S3 Glacier와 같은 더 저렴한 스토리지 클래스로 이동시켜 비용을 최적화할 수도 있습니다.
  • 다양한 기능: 정적 웹사이트 호스팅, 데이터 아카이빙, 빅데이터 분석을 위한 데이터 레이크 등 S3는 단순 파일 저장을 넘어 무궁무진한 활용 가능성을 가지고 있습니다. 파일 버전 관리, 수명 주기 정책을 통한 자동 삭제/이동 등의 기능은 운영 부담을 크게 줄여줍니다.

구체적인 전환 과정

기존 애플리케이션 코드를 수정하여 파일 업로드 로직을 S3와 연동하는 작업이 필요했습니다. Spring Boot 애플리케이션에서는 AWS SDK for Java를 사용하여 비교적 간단하게 구현할 수 있습니다.

  1. AWS SDK 의존성 추가: build.gradle 또는 pom.xml에 AWS S3 SDK 관련 의존성을 추가합니다.
    // build.gradle
    implementation 'software.amazon.awssdk:s3:2.20.26'
  2. S3 Client 설정: AWS 자격 증명(Access Key, Secret Key)을 사용하여 S3 Client를 생성합니다. 하지만 보안상 더 권장되는 방법은 IAM Role을 EC2 인스턴스에 부여하는 것입니다. 이렇게 하면 코드에 자격 증명을 하드코딩할 필요 없이 안전하게 S3에 접근할 수 있습니다.
    @Configuration
    public class AwsS3Config {
    
        @Value("${cloud.aws.region.static}")
        private String region;
    
        @Bean
        public S3Client s3Client() {
            return S3Client.builder()
                    .region(Region.of(region))
                    // IAM Role을 사용하면 credentialsProvider를 명시할 필요가 없습니다.
                    // .credentialsProvider(DefaultCredentialsProvider.create())
                    .build();
        }
    }
  3. 파일 업로드 로직 수정: 기존 java.io.File을 사용해 로컬에 저장하던 로직을 S3Client의 putObject 메소드를 사용하도록 변경합니다.
    @Service
    @RequiredArgsConstructor
    public class FileUploadService {
    
        private final S3Client s3Client;
    
        @Value("${cloud.aws.s3.bucket}")
        private String bucketName;
    
        public String uploadFile(MultipartFile multipartFile) throws IOException {
            String fileName = UUID.randomUUID().toString() + "_" + multipartFile.getOriginalFilename();
            
            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(fileName)
                    .contentType(multipartFile.getContentType())
                    .contentLength(multipartFile.getSize())
                    .build();
    
            s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(multipartFile.getInputStream(), multipartFile.getSize()));
    
            // 업로드된 파일의 URL 반환
            return s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(fileName)).toExternalForm();
        }
    }

이러한 변경을 통해 이제 모든 이미지 파일은 S3에 안전하게 저장됩니다. EC2 인스턴스는 더 이상 파일을 직접 보관하지 않으므로, 언제든지 동일한 사양의 다른 인스턴스로 교체되거나 여러 대로 확장되어도 서비스 연속성을 보장할 수 있는 상태 비저장(Stateless) 아키텍처의 기반을 마련하게 되었습니다.

3단계: 데이터베이스 독립 선언, Amazon RDS 도입

S3 도입으로 파일 저장 문제를 해결했지만, 여전히 애플리케이션 서버와 데이터베이스가 한 몸처럼 묶여있다는 근본적인 문제는 남아있었습니다. 둘은 서로 다른 자원 사용 패턴을 가집니다. 애플리케이션 서버는 CPU와 메모리를 많이 사용하는 반면, 데이터베이스는 I/O와 메모리에 더 민감합니다. 이 둘을 한 EC2 인스턴스에 두는 것은 마치 단거리 육상선수와 마라토너를 같은 훈련 방식으로 키우려는 것과 같습니다. 이 문제를 해결하기 위한 AWS의 솔루션은 RDS (Relational Database Service)입니다.

RDS는 왜 '신의 한 수'였는가?

RDS는 단순히 클라우드 서버에 데이터베이스 소프트웨어를 설치해주는 서비스가 아닙니다. 데이터베이스 운영과 관련된 모든 번거로운 작업을 AWS가 대신 관리해주는 '완전 관리형' 서비스입니다. 개발자로서 RDS를 도입하고 나서야 비로소 데이터베이스 관리의 고통에서 벗어나 애플리케이션 개발 자체에 집중할 수 있었습니다.

고려사항 EC2에 직접 설치 (Self-Hosted) Amazon RDS (Managed Service)
초기 설정 OS 설치, DB 소프트웨어 설치, 네트워크 설정, 보안 설정 등 모든 것을 직접 수행해야 함 콘솔 또는 CLI에서 몇 번의 클릭/명령으로 원하는 사양의 DB 인스턴스 즉시 생성
백업 및 복구 백업 스크립트를 직접 작성하고 스케줄링해야 함. 복구 절차도 복잡하고 시간이 오래 걸림 자동화된 일일 스냅샷 백업. 특정 시점으로 복구(Point-in-Time Recovery) 기능 기본 제공
고가용성 (HA) Primary-Secondary 구조, 클러스터링 등을 직접 구축하고 장애 시 Failover 로직을 구현해야 함 다중 AZ (Multi-AZ) 옵션을 체크하는 것만으로 다른 가용 영역에 동기식 스탠바이 복제본 자동 생성 및 장애 시 자동 Failover 지원
확장성 스케일 업/아웃을 위해 서비스 중단이 필요할 수 있으며, 읽기 전용 복제본(Read Replica) 구성이 복잡함 간단한 클릭으로 DB 인스턴스 사양 변경(스케일 업). 읽기 전용 복제본을 쉽게 추가하여 읽기 트래픽 분산(스케일 아웃)
패치 및 유지보수 OS 및 DB의 보안 패치, 버전 업그레이드를 직접 모니터링하고 적용해야 함 유지 관리 기간을 설정하면 AWS가 자동으로 패치 및 업그레이드를 수행

RDS로의 마이그레이션

RDS로 전환하는 과정은 생각보다 간단했습니다.

  1. RDS 인스턴스 생성: AWS 관리 콘솔에서 원하는 데이터베이스 엔진(MySQL), 버전, 인스턴스 사양을 선택하여 RDS 인스턴스를 생성합니다. 이때 중요한 것은 보안 그룹(Security Group) 설정입니다. EC2 인스턴스의 보안 그룹에서만 RDS의 데이터베이스 포트(MySQL의 경우 3306)로 접근할 수 있도록 인바운드 규칙을 설정하여 외부에서의 직접적인 접근을 차단해야 합니다.
  2. 데이터 마이그레이션: 기존 EC2의 MySQL 데이터를 새로운 RDS 인스턴스로 옮겨야 합니다. 데이터 양이 적다면 mysqldump와 같은 네이티브 도구를 사용하여 간단히 백업하고 복원할 수 있습니다. 데이터가 많고 서비스 중단을 최소화해야 한다면 AWS DMS (Database Migration Service)를 사용하는 것이 좋습니다. DMS는 거의 제로 다운타임에 가까운 마이그레이션을 지원합니다.
  3. 애플리케이션 연결 정보 변경: Spring Boot 애플리케이션의 application.yml 또는 application.properties 파일에서 데이터베이스 연결 정보(JDBC URL)를 기존 localhost에서 RDS 엔드포인트 주소로 변경합니다.
    # Before
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/my_database
        username: root
        password: password123
    
    # After
    spring:
      datasource:
        url: jdbc:mysql://my-rds-instance.ab12cd34ef56.ap-northeast-2.rds.amazonaws.com:3306/my_database
        username: admin
        password: new_secure_password

이로써 아키텍처는 [EC2 Web/API Server] - [S3 Object Storage] - [RDS Database] 라는, 각자의 역할이 명확하게 분리된 안정적인 구조를 갖추게 되었습니다. EC2 인스턴스는 이제 오직 애플리케이션 로직 처리에만 집중할 수 있게 되었고, 데이터베이스는 RDS가, 파일은 S3가 책임지는 이상적인 역할 분담이 이루어진 것입니다.

4단계: 아키텍처의 심장 교체, Lambda와 API Gateway

S3와 RDS 도입으로 인프라의 안정성은 크게 향상되었지만, 애플리케이션 자체는 여전히 거대한 모놀리식 덩어리였습니다. 웹 페이지를 렌더링하는 MVC 컨트롤러와 클라이언트에 JSON 데이터를 제공하는 REST API 컨트롤러가 같은 Spring Boot 프로젝트 안에 공존하고 있었습니다. 이는 두 가지의 서로 다른 요구사항을 가진 로직이 묶여있음을 의미합니다. 웹 페이지는 비교적 트래픽이 일정하지만, 특정 API는 이벤트나 마케팅에 따라 트래픽이 순간적으로 폭증할 수 있습니다. 이런 상황에서 EC2 하나에 모든 것을 두는 것은 여전히 비효율적이었습니다. 여기서 저는 MSA (Microservices Architecture)의 개념을 도입하기로 결정했고, 그 첫걸음으로 API 서버를 분리하기로 했습니다. 그리고 이 분리를 위해 선택한 기술이 바로 AWS LambdaAPI Gateway, 즉 서버리스(Serverless)입니다.

왜 EC2가 아닌 Lambda였을까?

API 서버를 분리한다고 했을 때, 가장 쉽게 떠올릴 수 있는 방법은 API용 Spring Boot 프로젝트를 하나 더 만들어 별도의 EC2 인스턴스에 배포하는 것입니다. 하지만 저는 이 방법 대신 서버리스 아키텍처를 선택했습니다. 그 이유는 다음과 같습니다.

서버리스 컴퓨팅은 개발자가 서버를 프로비저닝하거나 관리할 필요 없이 애플리케이션을 빌드하고 실행할 수 있도록 하는 클라우드 컴퓨팅 실행 모델입니다. 서버리스는 서버가 없다는 의미가 아니라, 서버 관리 작업이 AWS에 의해 처리된다는 것을 의미합니다.

AWS 공식 문서
  • 비용의 극적인 최적화: EC2는 실행 시간과 관계없이 인스턴스가 켜져 있는 동안 계속해서 비용이 발생합니다. 24시간 내내 요청이 거의 없는 심야 시간에도 비용은 동일합니다. 하지만 Lambda는 코드가 실행되는 시간(밀리초 단위)과 호출 횟수에 대해서만 비용을 지불합니다. API 호출이 없을 때는 비용이 전혀 발생하지 않습니다. 이는 특히 트래픽 변동성이 큰 API 서버에 압도적인 비용 효율성을 제공합니다.
  • 자동 확장성: EC2 기반의 API 서버는 트래픽 예측을 통해 Auto Scaling 설정을 미리 해두어야 합니다. 하지만 Lambda는 요청이 들어올 때마다 AWS가 알아서 필요한 만큼의 실행 환경을 즉시 생성하여 코드를 실행합니다. 갑작스러운 트래픽 폭증에도 별도의 설정 없이 수천, 수만 개의 동시 요청을 자동으로 처리해냅니다. 더 이상 스케일링 정책을 고민할 필요가 없습니다.
  • 운영 부담 제로: Lambda를 사용하면 OS 패치, 보안 업데이트, 서버 모니터링 등 인프라 관리에 신경 쓸 필요가 전혀 없습니다. 개발자는 오직 비즈니스 로직을 담은 '함수(Function)' 코드 작성에만 집중하면 됩니다.

API Gateway: 서버리스로 가는 관문

Lambda 함수는 그 자체로는 HTTP 요청을 직접 받을 수 없습니다. 이때 필요한 것이 바로 API Gateway입니다. API Gateway는 Lambda 함수를 외부 세계와 연결해주는 '대문' 역할을 합니다.

API Gateway는 단순한 프록시가 아닙니다. 다음과 같은 강력한 기능들을 제공합니다.

  • 요청 라우팅: /users, /products/{productId}와 같은 특정 HTTP 경로(Path)와 메소드(GET, POST 등) 요청을 해당 기능을 처리하는 Lambda 함수로 정확하게 연결해줍니다.
  • 인증 및 권한 부여: API Key 발급, AWS IAM, Cognito User Pool, Lambda Authorizer 등을 통해 API에 대한 접근 제어를 손쉽게 구현할 수 있습니다.
  • 요청/응답 변환 및 유효성 검사: 클라이언트로부터 받은 요청을 Lambda가 처리하기 쉬운 형태로 변환하거나, 요청 본문의 유효성을 미리 검사하여 불필요한 Lambda 호출을 막을 수 있습니다.
  • 사용량 제한(Throttling) 및 캐싱: 특정 클라이언트나 API에 대해 초당 요청 수를 제한하여 과도한 호출로부터 백엔드를 보호하고, 반복적인 요청에 대한 응답을 캐싱하여 Lambda 호출 비용을 줄이고 응답 속도를 향상시킬 수 있습니다.

점진적인 전환 전략: 스트랭글러 피그 패턴 (Strangler Fig Pattern)

기존 EC2의 모놀리식 API를 한 번에 모두 Lambda로 옮기는 것은 매우 위험하고 현실적이지 않습니다. 저는 마틴 파울러가 주창한 스트랭글러 피그 패턴을 적용하기로 했습니다. 이는 오래된 거대한 나무(모놀리식)를 서서히 감싸며 자라 결국 나무를 대체하는 무화과나무(Fig)처럼, 기존 시스템의 기능을 하나씩 새로운 시스템으로 점진적으로 교체해 나가는 전략입니다.

  1. 가장 간단한 API부터 시작: 가장 먼저, 기능적으로 독립적이고 비즈니스 중요도가 비교적 낮은 GET 요청 API(예: 상품 목록 조회)를 첫 번째 마이그레이션 대상으로 선정했습니다.
  2. Lambda 함수 작성: 기존 Spring 코드의 비즈니스 로직을 가져와 Java로 Lambda 함수를 작성했습니다. Lambda는 다양한 언어(Node.js, Python, Go, Java 등)를 지원하지만, 기존 로직 재활용을 위해 Java를 선택했습니다. 단, Java는 JVM의 특성상 초기 실행 시 시간이 더 걸리는 콜드 스타트(Cold Start) 이슈가 있으므로, 응답 시간에 민감한 API라면 이 점을 고려해야 합니다.
    import com.amazonaws.services.lambda.runtime.Context;
    import com.amazonaws.services.lambda.runtime.RequestHandler;
    import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
    import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
    
    // APIGatewayProxyRequestEvent: API Gateway가 보내주는 요청 정보를 담은 객체
    // APIGatewayProxyResponseEvent: API Gateway에 반환해야 할 응답 형식을 담은 객체
    public class GetProductsHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    
        @Override
        public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
            // 1. 데이터베이스에서 상품 목록 조회 (RDS 접속)
            // ProductService productService = new ProductService();
            // List<Product> products = productService.findAll();
    
            // 2. 조회된 결과를 JSON 문자열로 변환
            // String jsonBody = new Gson().toJson(products);
            
            String jsonBody = "[{\"id\":1, \"name\":\"상품A\"}, {\"id\":2, \"name\":\"상품B\"}]"; // 예시 데이터
    
            // 3. API Gateway 형식에 맞는 응답 객체 생성
            APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
            response.setStatusCode(200);
            response.setBody(jsonBody);
            
            // CORS 처리를 위한 헤더 추가
            // Map<String, String> headers = new HashMap<>();
            // headers.put("Access-Control-Allow-Origin", "*");
            // response.setHeaders(headers);
    
            return response;
        }
    }
  3. API Gateway 설정: AWS 콘솔에서 /products 리소스와 GET 메소드를 생성하고, 이를 방금 작성한 Lambda 함수와 'Lambda 프록시 통합' 방식으로 연결했습니다.
  4. 클라이언트 수정 및 테스트: 웹 클라이언트(React, Vue 등)의 API 호출 주소를 기존 EC2 서버 주소에서 새로 생성된 API Gateway 엔드포인트 URL로 변경하고 충분한 테스트를 진행했습니다.
  5. 반복: 성공적으로 하나의 API가 이전되면, 다음 API를 대상으로 이 과정을 계속 반복합니다. 복잡한 로직을 가진 API를 점차적으로 이전하면서,最终적으로 EC2에는 웹 페이지만을 서빙하는 역할만 남게 됩니다.

결과: 이제 EC2 인스턴스는 순수하게 웹 서버의 역할만 담당하게 되었고, 모든 API 호출은 API Gateway를 통해 Lambda에서 처리됩니다. [EC2 Web Server] + [API Gateway -> Lambda] + [S3] + [RDS] 라는 현대적인 아키텍처가 완성되었습니다. 이를 통해 웹 서버와 API 서버의 배포 주기를 분리하고, 트래픽에 따라 독립적으로 확장하며, 사용한 만큼만 비용을 지불하는 효율적인 시스템을 구축할 수 있었습니다.

5단계: 트래픽 급증 대비, 로드 밸런서와 Auto Scaling

API 서버는 Lambda로 전환하여 자동 확장의 이점을 누리게 되었지만, 아직 한 가지 문제가 남아있습니다. 바로 사용자들이 직접 접속하는 웹 서버, 즉 EC2 인스턴스가 여전히 단 하나라는 점입니다. 이 EC2 인스턴스에 장애가 발생하면 사용자들은 웹 페이지 자체에 접근할 수 없게 됩니다. 또한, 사용자가 늘어나 이 EC2 인스턴스의 부하가 높아졌을 때 대처할 방법이 없습니다. 이 문제를 해결하기 위해 로드 밸런서(Load Balancer)Auto Scaling을 도입하여 웹 서버 계층의 가용성과 확장성을 확보해야 합니다.

로드 밸런서 선택의 기로: ALB vs NLB

AWS의 Elastic Load Balancing (ELB)는 들어오는 트래픽을 여러 대상(EC2 인스턴스, 컨테이너 등)에 자동으로 분산시켜주는 서비스입니다. ELB에는 여러 유형이 있지만, 가장 대표적인 것은 Application Load Balancer (ALB)Network Load Balancer (NLB)입니다. 두 로드 밸런서는 동작하는 계층과 기능이 다르기 때문에, 우리 서비스의 요구사항에 맞는 것을 신중하게 선택해야 합니다.

구분 Application Load Balancer (ALB) Network Load Balancer (NLB)
OSI 7계층 7계층 (애플리케이션 계층) 4계층 (전송 계층)
주요 프로토콜 HTTP, HTTPS, gRPC TCP, UDP, TLS
라우팅 규칙 요청의 내용(Host 헤더, URL 경로, 쿼리 파라미터 등)을 보고 분기 가능. 경로 기반 라우팅 (/users/*는 A서버 그룹, /images/*는 B서버 그룹) 등 복잡한 라우팅에 유리. IP 주소, 포트 번호 등 TCP/IP 헤더 정보만 보고 패킷을 전달. 매우 빠른 속도로 트래픽 분산.
성능 지능적인 라우팅 기능으로 인해 NLB보다 약간의 지연 시간이 있을 수 있음. 매우 낮은 지연 시간과 초당 수백만 건의 요청을 처리할 수 있는 초고성능.
IP 주소 IP 주소가 동적으로 변경됨. 고정 IP (Elastic IP)를 할당할 수 있어, IP 기반으로 접근 제어가 필요한 경우 유리함.
주요 사용 사례 일반적인 웹 애플리케이션, 마이크로서비스 아키텍처, 컨테이너 기반 애플리케이션(ECS, EKS) 온라인 게임, 스트리밍, IoT와 같이 극도로 높은 성능과 낮은 지연 시간이 요구되는 TCP/UDP 기반 서비스

우리의 웹 서버는 HTTP 프로토콜을 사용하며, 향후 URL 경로에 따라 다른 EC2 그룹으로 트래픽을 분산시킬 가능성도 있습니다. 따라서 OSI 7계층에서 동작하며 지능적인 라우팅이 가능한 ALB (Application Load Balancer)가 가장 적합한 선택입니다.

Auto Scaling으로 탄력적인 인프라 완성하기

로드 밸런서만으로는 완벽하지 않습니다. 트래픽을 분산할 대상 EC2 인스턴스가 단 하나라면 로드 밸런서는 무용지물입니다. 이때 필요한 것이 바로 Auto Scaling Group (ASG)입니다.

Auto Scaling은 미리 정의된 조건에 따라 EC2 인스턴스 수를 자동으로 늘리거나 줄이는 기능입니다. ALB와 ASG는 환상의 조합을 이룹니다.

  1. 시작 템플릿/구성 생성: 어떤 종류의 EC2 인스턴스(AMI, 인스턴스 유형, 보안 그룹 등)를 생성할 것인지 정의하는 템플릿을 만듭니다.
  2. Auto Scaling Group 설정: 이 템플릿을 사용하여 인스턴스를 관리할 그룹을 생성합니다. 최소/최대/희망 용량(인스턴스 수)을 설정합니다. 예를 들어, 평소에는 2대를 유지하고, 최대 10대까지 늘어날 수 있도록 설정할 수 있습니다.
  3. 조정 정책(Scaling Policy) 정의: 언제 인스턴스를 늘리고 줄일지를 결정하는 규칙을 설정합니다. 가장 많이 사용하는 '대상 추적 조정 정책'은 특정 지표(예: 모든 인스턴스의 평균 CPU 사용률)가 목표 값을 유지하도록 인스턴스 수를 조절합니다. 예를 들어, '평균 CPU 사용률을 50%로 유지'하도록 설정하면, 트래픽이 몰려 CPU 사용률이 50%를 넘어서면 ASG가 자동으로 새로운 EC2 인스턴스를 생성하여 ALB에 등록합니다. 반대로 트래픽이 줄어 CPU 사용률이 50% 아래로 떨어지면 불필요한 인스턴스를 종료하여 비용을 절감합니다.
  4. ALB와 연동: 생성한 Auto Scaling Group을 ALB의 대상 그룹(Target Group)으로 지정합니다. 그러면 ASG가 새로 시작하는 모든 인스턴스는 자동으로 ALB에 등록되어 트래픽을 수신하고, 종료되는 인스턴스는 ALB에서 자동으로 제외됩니다.

이제 사용자의 요청은 ALB를 통해 여러 가용 영역에 분산된 EC2 인스턴스 중 하나로 전달됩니다. 특정 인스턴스에 장애가 발생하면 ALB는 이를 감지하고 해당 인스턴스로는 더 이상 트래픽을 보내지 않으며, ASG는 장애가 발생한 인스턴스를 종료하고 새로운 건강한 인스턴스를 즉시 생성하여 대체합니다. 이로써 우리는 소수의 인원으로도 24시간 365일 안정적으로 동작하는, 진정한 고가용성(High Availability) 및 탄력성(Elasticity)을 갖춘 아키텍처를 완성하게 됩니다.

결론: 진화는 계속된다

단 하나의 EC2 인스턴스에서 시작했던 아키텍처는 S3, RDS, Lambda, API Gateway, 그리고 ALBAuto Scaling을 거치며 확장 가능하고, 안정적이며, 비용 효율적인 현대적인 클라우드 아키텍처로 진화했습니다. 이 여정은 단순히 최신 기술을 적용하는 과정이 아니었습니다. 서비스가 가진 문제를 정확히 진단하고, 각 단계에서 가장 적합한 AWS 서비스를 선택하여 문제를 해결해 나가는 과정이었습니다.

중요한 것은 완벽한 아키텍처는 없다는 사실입니다. 아키텍처는 비즈니스의 성장에 따라 끊임없이 변화하고 발전해야 하는 살아있는 유기체와 같습니다. 저희의 다음 목표는 다음과 같습니다.

  • CI/CD 파이프라인 구축: AWS CodePipeline, CodeDeploy와 같은 서비스를 활용하여 코드 변경 사항을 자동으로 빌드, 테스트, 배포하는 파이프라인을 구축하여 개발 생산성을 극대화할 것입니다.
  • 인프라 코드화 (Infrastructure as Code, IaC): AWS CloudFormation이나 Terraform을 사용하여 현재의 아키텍처를 코드로 관리함으로써, 인프라를 빠르고 일관되게 복제하고 변경 사항을 추적할 수 있도록 개선할 계획입니다.
  • 모니터링 및 로깅 강화: Amazon CloudWatch, AWS X-Ray 등을 활용하여 시스템 전반의 성능 지표와 로그를 중앙에서 관리하고, 잠재적인 문제를 사전에 감지하고 대응하는 체계를 갖출 것입니다.

이 글을 통해 모놀리식 아키텍처로 고민하고 있는 많은 개발자분들이 작은 단계부터 시작하여 점진적으로 시스템을 개선해 나갈 용기를 얻으셨으면 좋겠습니다. 클라우드의 무한한 가능성을 활용하여 여러분의 서비스를 더욱 견고하고 유연하게 만들어나가시길 응원합니다. 이 여정에 대한 더 궁금한 점이나 의견이 있다면 언제든지 공유해주시기 바랍니다.

Post a Comment