본문 바로가기

카테고리 없음

[goggles 프로젝트: 아키텍처] 일단 집을 지어보자

개요

프로젝트를 시작하기 전 가장 먼저 해야 할 일은 요구사항을 분석하고, 사용자 흐름도를 작성하는 것이다.
이 과정을 통해 서비스의 성격과 배포 주기를 고려하여 도메인의 범위를 나누고, 각 기능이 어떤 단위로 구성되어야 하는지를 정의할 수 있다.

 

그 다음으로 중요한 단계는, 서비스를 실제로 운영하기 위한 인프라 아키텍처를 설계하는 일이라고 생각한다.
기획 단계에서는 기능 중심의 설계가 이루어지지만, 인프라 설계는 단순한 구조 선택을 넘어 실질적인 운영 비용과 직결되는 중요한 의사결정 과정이다.

 

특히 이번 프로젝트에서는 제한된 운영 비용이라는 현실적인 조건 속에서,
어떤 기준으로 인프라 구성을 선택하고, 어떤 방식으로 아키텍처를 발전시켜 나갔는지를 단계적으로 정리해보고자 한다.


초기 인프라 아키텍처

 

1. 고가용성 (High Availability) 확보

서비스는 언제든지 접근 가능해야 하며, 특정 장애 상황에서도 지속적으로 동작해야 한다. 이를 위해 모든 주요 컴포넌트를 2개의 가용 영역(AZ) 에 이중화하여 구성하였다.

구성 방식은 다음과 같다.

  • 모든 애플리케이션 서버를 2개의 AZ에 분산 배치
  • Kafka Cluster를 다중 AZ 구조로 구성
  • PostgreSQL을 Primary / Replica 이중화 구조로 운영
  • 단일 AZ 장애 발생 시에도 서비스 지속 가능

이러한 구조를 통해 단일 AZ 장애가 발생하더라도 서비스가 중단되지 않도록 설계하였다.

특히 실제 운영 환경에서는 네트워크 장애나 AZ 장애가 발생할 가능성을 배제할 수 없기 때문에,
단일 장애 지점(SPOF, Single Point of Failure) 을 최소화하는 것을 중요한 기준으로 삼았다.

💡하나의 AZ가 완전히 다운되어도 나머지 AZ의 인스턴스들이 트래픽을 처리할 수 있어야 한다. ALB가 헬스 체크를 통해 정상 인스턴스로만 트래픽을 자동 라우팅한다.

 

# Multi-AZ #ALB #Kafka #KRaft #PostgreSQL #Replica #ElastiCache #Redis


2. 무중단 배포 (Zero Downtime Deployment)

서비스를 운영하면서 새로운 기능을 배포하거나 버그를 수정하는 과정은 필수적이다. 하지만 배포 과정에서 서비스가 중단된다면 사용자 경험에 큰 영향을 줄 수 있다.

이를 해결하기 위해 롤링 업데이트 기반 배포 전략을 적용하였다.

  • CodeDeploy를 활용한 자동 배포 구성
  • AZ1 → AZ2 순서로 순차적 롤링 배포 진행
  • 기존 인스턴스를 유지한 상태에서 신규 인스턴스 교체
  • 배포 중에도 서비스 지속 가능

CI/CD 파이프라인은 GitHub Actions → ECR → CodeDeploy 의 흐름으로 구성된다. 코드가 푸시되면 자동으로 테스트, 빌드, 이미지 푸시까지 진행되며, CodeDeploy가 롤링 방식으로 배포를 완료한다.

# GitHub Actions → ECR → CodeDeploy

GitHub Actions
  1. test    # 유닛 테스트 실행
  2. build   # Docker 이미지 빌드
  3. push    # ECR에 이미지 푸시
       ↓
ECR            # 이미지 저장소
       ↓
CodeDeploy     # 롤링 업데이트
  AZ1 인스턴스 교체 → 헬스 체크 통과 확인
  AZ2 인스턴스 교체 → 헬스 체크 통과 확인

✅ Result 이 방식은 전체 서비스를 한 번에 교체하지 않고, 일부 인스턴스만 순차적으로 교체하기 때문에 배포 중에도 서비스가 정상적으로 유지된다. 결과적으로 다운타임 없이 안정적인 서비스 배포 환경을 구성할 수 있었다.

 

#GitHub Actions #AWS ECR #CodeDeploy #Rolling Update


3. 보안 계층화 (Security Layering)

외부 접근 경로를 최소화하고 내부 시스템을 보호하기 위해 Public Subnet과 Private Subnet을 명확하게 분리하는 구조를 적용하였다.

외부와 내부 네트워크의 역할은 다음과 같이 구분하였다.

Public Subnet

외부에서 접근 가능한 최소한의 컴포넌트만 배치하였다.

‧ALB (Application Load Balancer)
‧Bastion Host
‧Internet Gateway
‧NAT Gateway

 

Private Subnet

실제 비즈니스 로직이 동작하는 서버는 모두 Private Subnet 내부에 배치하였다.

‧Application Servers (전 서비스)
‧Kafka Cluster (3노드)
‧PostgreSQL (Primary / Replica)
‧ElastiCache Redis

 

클라이언트는 Public Subnet에 위치한 ALB에만 접근할 수 있으며, 직접 내부 서버에 접근하는 것은 불가능하도록 구성하였다.

운영자가 내부 서버에 접근해야 할 경우, Bastion Host를 통해서만 SSH 접속이 허용된다.

⚠️ Security Point Private Subnet 내의 서버들은 인바운드 인터넷 트래픽을 직접 받지 않는다.
아웃바운드 통신(패키지 설치, 외부 API 호출 등)은 NAT Gateway를 통해서만 허용된다.

 

이 구조를 통해 외부에서 내부 서비스로의 직접 접근을 차단하고,
보안 사고 발생 가능성을 최소화하는 계층형 네트워크 구조를 구성하였다.


#VPC #Public/Private Subnet #Bastion Host #NAT Gateway #Security Group


4. 이벤트 기반 아키텍처 (Event-Driven Architecture)

마이크로서비스 환경에서는 서비스 간 결합도를 낮추는 것이 매우 중요하다.
특히 결제, 주문 완료와 같은 핵심 기능은 여러 서비스에 영향을 미치기 때문에 동기 방식 호출만으로는 확장성과 안정성을 확보하기 어렵다.

이를 해결하기 위해 Kafka 기반 이벤트 드리븐 아키텍처를 적용하였다.

예를 들어 결제가 완료되면 다음과 같은 흐름이 발생한다.

Payment Server
      ↓  (결제 완료 이벤트 발행)
    Kafka
      ↓  (이벤트 구독)
Notification Server
      ↓
User에게 알림 전송

 

Payment Server는 결제 완료 이벤트를 Kafka 토픽에 발행하기만 하면 되며, Notification Server가 이를 구독해 알림을 전송한다. 두 서비스는 서로의 존재를 직접 알 필요가 없다.

이 방식의 장점은 다음과 같다.

  • ✔ 서비스 간 직접 호출 감소
  • ✔ 결합도 (Low Coupling) 감소
  • ✔ 이벤트 기반 확장성 확보
  • ✔ 장애 발생 시 영향 범위 최소화

💡 Why Kafka? 결제와 같은 핵심 이벤트는 반드시 처리되어야 한다. Kafka는 메시지를 디스크에 영속적으로 저장하고, Consumer가 오프라인 상태였더라도 재기동 시 메시지를 재처리할 수 있다. 이를 통해 안정성과 확장성을 동시에 확보하는 것을 목표로 하였다.

 

#Apache #Kafka #KRaft Mode #Async #Messaging Loose #Coupling #Event Sourcing

 


한계점

위 구조는 가용성 확보를 위한 여러 장치를 갖추고 있지만, 모든 부분이 이상적인 형태로 구현된 것은 아니다. 프로젝트의 기간과 비용이라는 현실적인 제약 속에서 다음과 같은 아쉬운 점이 존재한다.

  • 구성 복잡도와 운영 비용
    • 다중 AZ 이중화 구성은 안정성 측면에서 분명히 좋은 선택이지만, Kafka 클러스터, PostgreSQL Replica 등 모든 컴포넌트를 이중화하는 구조는 운영 비용이 상당히 높다.
    • 제한된 프로젝트 기간과 예산을 고려하면 지속적으로 유지하기 부담스러운 구성이라고 생각했다.
  • Kafka KRaft 과반수 법칙
    • KRaft 모드에서 Kafka는 리더 선출 시 과반수(quorum) 투표 방식을 사용한다.
    • 이상적으로는 3개의 AZ에 Controller를 1개씩 분산하는 구조가 가장 안정적이지만, 현재는 AZ가 2개뿐이기 때문에 AZ1에 2개, AZ2에 1개로 구성하였다. 이 경우 AZ1 장애가 발생하면 Controller 과반수를 충족하지 못하게 되어 Kafka 클러스터가 중단될 수 있는 한계가 존재한다.
    • 3개 AZ 구성이 어려운 환경에서는 피하기 어려운 트레이드오프이며, Kafka를 핵심 이벤트 처리 시스템으로 활용하려는 목표에도 일정 부분 영향을 줄 수 있는 요소라고 판단하였다.
  • Application Level 가용성
    • 인프라 레벨의 이중화에는 신경을 썼지만, 애플리케이션 레벨의 가용성은 이번 구성에서 다루지 못했다. 특히 Eureka Server와 Gateway Server는 모든 서비스의 요청이 반드시 거쳐야 하는 핵심 컴포넌트임에도 불구하고, 현재 단일 인스턴스로만 운영되고 있다.
    • 두 서버에 장애가 발생하면 인프라가 정상이더라도 서비스 전체가 중단될 수 있어, 사실상 애플리케이션 레벨의 SPOF가 존재하는 상태다. 

개선된 인프라 아키텍처

앞서 언급한 한계점들을 인식하고, 현실적인 제약 안에서 다음과 같이 구조를 개선하였다.

 

1️⃣ 비용 문제로 AZ 이중화 미적용

다중 AZ 구성은 가용성 측면에서 이상적이지만, 프로젝트 기간과 운영 비용을 고려하여 AZ 이중화는 적용하지 않기로 결정하였다. 대신 애플리케이션 레벨에서 가용성을 확보하는 방향으로 전환하였다.

 

 

2️⃣ Gateway / Eureka Server 이중화

AZ 이중화를 걷어낸 대신, 모든 요청이 반드시 거치는 Gateway Server와 Eureka Server를 각각 2대로 이중화하였다.

인프라 레벨 이중화는 포기하더라도, 애플리케이션 레벨의 SPOF만큼은 제거하는 것을 목표로 하였다.

 

 

3️⃣LectureServer 별도 EC2 분리

특정 서비스의 부하가 다른 서비스에 영향을 주는 것을 방지하기 위해 트래픽 집중이 예측되는 강의 서비스는 다른 서비스와 EC2를 분리하여 구성하였다. 또한 향후 강의 파일(영상) 관련 서비스가 추가될 가능성을 고려하여, 해당 기능이 강의 서비스와 함께 확장될 수 있도록 초기 단계부터 독립적인 인프라 구조로 구성하였다.

 

 

4️⃣ Kafka 3대 KRaft 구성으로 과반수 법칙 적용

기존 2 AZ 분산 구성의 문제를 해결하기 위해, Kafka 브로커를 3대로 구성하고 단일 AZ 내에서 운영하는 방식으로 변경하였다.

이를 통해 KRaft 과반수 법칙(3대 중 2대 이상 정상)을 정상적으로 적용할 수 있게 되었다.


이후 고려사항

1. Redis

비용 절감을 위해 ElastiCache 대신 EC2에 직접 Redis를 띄우는 방식 검토 중이다.

이 경우 고가용성 확보를 위해 Redis Sentinel을 직접 구성하는 방향을 고려하고 있다.

 

2. PostgreSQL Replica

프로젝트 기간과 비용을 고려하여 RDS Replica 제거도 검토 중이다.

읽기 트래픽이 충분히 크지 않다면 단일 인스턴스로도 운영 가능하다고 판단하고 있다.

 

3. 모니터링 구성 (Prometheus + Grafana)

현재 서비스의 상태를 실시간으로 파악하기 위해 Prometheus + Grafana 기반의 모니터링 구성을 도입할 예정이다.

모니터링 도구를 애플리케이션 서버와 함께 운영하면 서버 부하 시 모니터링 자체도 영향을 받기 때문에,
독립된 인스턴스로 분리하는 방향을 고민 중이다.

  • Prometheus: 각 서비스의 CPU, 메모리, 요청 수, 응답 시간 등 메트릭 수집
  • Grafana: 수집된 메트릭을 대시보드로 시각화, 임계치 초과 시 알림 설정 가능
  • Spring Boot Actuator + Micrometer를 통해 각 서비스에서 메트릭 노출

🤔 모니터링 서버를 별도 EC2로 분리할 경우 비용이 추가된다. 초기에는 기존 EC2에 함께 올리고, 트래픽이 늘어나는 시점에 분리하는 단계적 접근도 고려 중이다.


RDS 이미 Multi-AZ 제공하는데 왜 나누었는가 ?

MSA 환경에서 RDS 왜 단일로 제공하는가 ?

Cloud Front 서비스 추가 이유 ?

code deploy 무중단 배포 방식은 ?