이번주는 주로 백엔드-인프라 연동, 프론트-백엔드 연동을 진행했다.
일단 백엔드-인프라 연동을 하면서 예상치 못한 문제들이 많이 있었다...
1. SSM으로 Private EC2 접근하기
아키텍처를 설계할 때는 별 생각없이 짰는데, 생각해보니 Private EC2에 접근하는 방법이 없었다. 생각해보면 EC2는 Private Subnet에 있고, 사용자는 오직 ALB를 통해 서비스에 접근한다. 즉, 개발자 입장에서 EC2에 직접 접근할 방법이 없었던 것이다.
보통이라면 EC2에 탄력적 IP(Elastic IP)를 부여하고 SSH로 접속했겠지만, Private Subnet의 EC2는 외부에서 접근 가능한 퍼블릭 IP를 가지면 안 된다.
그래서 검색해보니, Private Subnet의 EC2에 접근하는 방법은 대표적으로 두 가지가 있었다.
- Bastion Host 사용하기
- SSM 사용하기
일단 이 두 개를 살펴보겠다.
1. Bastion Host
Bastion Host란, 내부 네트워크에 접근하기 위한 '보안 관문 역할을 하는 서버'다. 외부에서 바로 내부 서버에 접근하지 못하도록 막고,
Bastion Host를 통해서만 접속하도록 우회 경로를 제공해 보안성과 추적성을 높인다.
AWS에서 Bastion Host란?
AWS에서는 Private Subnet에 있는 EC2에 접속하기 위해, Public Subnet에 배치한 EC2 인스턴스를 Bastion Host로 사용한다.
보안을 위해 내부 EC2는 퍼블릭 IP를 갖지 않지만, Bastion Host는 퍼블릭 IP를 할당받고 22번 포트(SSH)를 열어두어 외부 개발자가 여기에 접속한 후 내부 EC2로 들어갈 수 있도록 중계 역할을 한다.
즉, 지금처럼 외부에서 Private EC2에 접속하려면 Bastion Host가 반드시 필요하다!
2. SSM(Session Manager) 사용하기
SSM (AWS Systems Manager) 는 AWS에서 EC2나 기타 리소스를 SSH 없이, 퍼블릭 IP 없이, 브라우저나 CLI로 관리할 수 있게 해주는 서비스다. 원래는 EC2에 접속하려면 반드시 퍼블릭 IP를 부여하거나, 아니면 Bastion Host를 통해 내부로 우회 접속해야 했다.
그런데 SSM Session Manager를 사용하면?
- 퍼블릭 IP ❌ 필요 없음
- Bastion Host ❌ 필요 없음
- SSH 키 ❌ 필요 없음
- 브라우저 콘솔이나 AWS CLI로 바로 접속 가능!!
즉, 보안은 더 강화되면서도 관리 편의성은 훨씬 좋아지는 구조에 심지어 무료이다...
그래서 우리는 Bastion Host 대신 SSM을 사용하기로 결정했다.
SSM을 설정하고 나니, 더 이상 Bastion Host 없이도 Private Subnet에 있는 EC2에 바로 접근할 수 있게 되었다!
SSH 키도, 퍼블릭 IP도 필요 없이 브라우저에서 EC2에 직접 접속할 수 있다는 점이 정말 편리하고 좋은 것 같다...
2. Spring Boot가 NAT 없이 내려가는 문제: ECR + EC2 + VPC Endpoint
우리의 기존 아키텍처에는 NAT Gateway가 없었는데, Docker 설치와 Spring Boot 배포를 위해 임시로 NAT를 생성해 사용했다.
Spring Boot까지 정상적으로 실행된 걸 확인하고 NAT를 제거했는데, 갑자기 Spring Boot 애플리케이션이 계속 종료되는 문제가 발생했다. 코드에 문제가 있나 싶었지만, NAT를 다시 붙이니 애플리케이션이 정상 실행됐다.
Spring Boot 내부에서 외부 API를 호출하는 부분도 없었고, S3나 DynamoDB는 VPC Endpoint를 통해 접근 중이었기 때문에 NAT가 필요한 이유를 도저히 알 수 없었다.
그래서 외부 요청이 발생하는지 확인하기 위해 VPC Flow Logs를 CloudWatch Logs에 연동했다.
로그를 분석해보니,
-> Spring Boot 애플리케이션 실행 과정에서 Docker가 Docker Hub에 이미지 요청을 보낸 것이 확인됐다!
즉, Docker Hub에서 이미지 pull 시 외부 인터넷이 필요했던 것! NAT를 없애니 외부 통신이 차단되어 애플리케이션이 죽었던 것이었다...
그런데 NAT는 비싸다...
- NAT Gateway는 시간당 과금 + 데이터 전송량에 따라 추가 요금 발생
- 비용 최적화를 위해 대안을 찾기로 결정했다.
해결책: ECR + EC2 + VPC Endpoint
그렇게 우리가 찾은 대안은 ECR + EC2 + VPC Endpoint다.
ECR이란?
ECR은 컨테이너(Docker) 이미지를 저장, 관리, 배포할 수 있는 완전관리형(Container Registry) 서비스이다.
Docker로 만든 애플리케이션을 배포하려면 이미지를 저장해둘 장소가 필요한데
- 로컬 PC에 저장하면 서버에서 쓸 수 없다.
- Docker Hub는 퍼블릭이거나 속도가 느릴 수 있다.
ECR은 AWS 내에서 프라이빗하게 이미지를 보관하고, EC2, ECS, EKS 등에서 빠르게 가져올 수 있게 해주는 저장소이다.
EC2, ECS, EKS와 같이 많이 사용한다.
ECS란?
AWS에서 제공하는 컨테이너 오케스트레이션 서비스, 즉, 컨테이너를 배포·실행·스케일링·관리하는 완전관리형 서비스다.
ECR에 있는 Docker 이미지를 ECS가 가져와서 서버(EC2 또는 AWS가 관리하는 Fargate) 위에서 실행시키는 것!
자동 스케일링, 배포 관리 등을 제공해준다.
왜 ECS는 안 썼을까?
- Fargate는 사용량 기반 과금 → 대규모 실시간 채팅 서비스에는 비용 부담
- ECS EC2 모드도 비용과 관리 부담이 큼
- 우리는 이미 EC2를 사용하고 있었기 때문에,
👉 EC2 + ECR 조합으로 충분하다고 판단했다!
그래서 EC2를 그대로 사용하면서 Docker에서 pull을 NAT 없이 받기 위해 ECR을 사용하기로 했다!
그리고 결과는 성공~
이제 NAT 없이도 안전하고 비용 효율적으로 Spring Boot를 배포할 수 있게 되었다!
3. 도메인 이슈 해결
프론트엔드에서도 도메인을 연결하는 과정에서 예상치 못한 이슈가 발생했다. 일반적으로는 가비아(Gabia)와 같은 도메인 등록 업체에서 도메인을 구입한 뒤, Route 53에 NS(Name Server) 레코드를 등록하여 도메인을 AWS와 연결하는 방식이 많이 사용된다.
하지만 이번에는 내도메인.한국이라는 무료 도메인 서비스를 사용했는데, 이곳은 NS 레코드 등록 기능을 제공하지 않았다.
처음에는 도메인을 다시 가비아 등에서 유료로 구입할지 고민했지만, 다행히 내도메인.한국은 CNAME 레코드 등록은 지원했기 때문에
AWS ACM 인증서 생성 시 제공된 CNAME 레코드를 그대로 등록하여 도메인 소유권 인증과 HTTPS 연결을 정상적으로 완료할 수 있었다.
원래 계획은 Route 53 + S3 + CloudFront 조합으로 정적 사이트를 배포하는 것이었다. 하지만 NS 레코드를 등록할 수 없는 환경에서는 Route 53을 사용할 수 없기 때문에, 이미 도메인 업체에서 CNAME 설정만으로 연결이 가능하다면 굳이 Route 53은 필요하지 않다고 판단했다.따라서 최종적으로는 S3 + CloudFront만 사용하는 구조로 배포를 완료했다.
백엔드 역시 같은 방식으로 처리했다. 도메인에 CNAME 레코드를 등록하여 ACM 인증서를 발급받고, 해당 인증서를 ALB에 연결하여, HTTPS 기반으로 도메인과 백엔드 서버를 연동했다.
4. 기타 문제들
기타 문제들은 대부분 보안 그룹 설정 때문이었다.
접속이 안 되거나 통신이 되지 않는 경우, 알고 보면 보안 그룹 인바운드나 아웃바운드 규칙이 빠져 있었던 것이다.
이번 경험을 통해 보안 그룹이 얼마나 중요한지, 그리고 작은 설정 하나가 전체 통신에 영향을 줄 수 있다는 점을 제대로 느꼈다...ㅜㅜ
최종 아키텍처
그렇게 위 수정사항들을 반영한 최종 아키텍처는 다음과 같다!

번외) 부하테스트
지금까지 구현한 것이 주어진 요건을 충족하는지 부하테스트도 해보았다.
1. 웹소켓 연결 지연 테스트
먼저 웹소켓 연결 지연을 측정하기 위해 k6 라이브러리를 사용해 자바스크립트 환경에서 테스트를 수행했다.
시나리오는 다음과 같다:
- 동시 가상 사용자 (VU): 500명
- 세션 유지 시간: 약 10초
- 각 사용자당 메시지 송신 횟수: 2회
테스트 결과는 매우 긍정적이었다.
- 연결 성공률: 100% (3,000/3,000 세션 성공)
- 평균 연결 지연: 220ms
- 95% 연결 지연: 1초 이하
- 최대 연결 지연: 1.21초 (단일 사용자 기준, 여유 있음)
- 총 메시지 전송: 6,000개
- 평균 메시지 전송 속도: 96.6개/초
결론적으로, 웹소켓 연결 단계에서는 지연 문제가 거의 없다고 볼 수 있다.
아래는 전체 결과이다.

2. 웹소켓 수신 지연 테스트
그러나 실제 요구사항은 메시지 수신 지연이 2초 이내여야 한다는 것이었다.
우리 시스템은 STOMP 프로토콜과 WebSocket(ws)을 함께 사용하고 있었지만, k6는 STOMP를 지원하지 않아 직접적인 테스트가 어려웠다.
이에 따라, 백엔드에서 수신 즉시 응답을 보내주는 테스트용 웹소켓 엔드포인트를 별도로 만들었고, 클라이언트에서는 메시지를 전송한 시간과 응답을 수신한 시간을 비교해 실제 수신 지연 시간을 측정했다.
목표는 단체 채팅방 환경에서, 지연 시간이 2초 이하인 상태에서 수용 가능한 최대 사용자 수를 파악하는 것이었다.
- 단체 채팅방 환경을 가정해 사용자 수를 점점 늘려가며 지연 시간을 측정
|
사용자 수
|
평균 지연(ms)
|
최소 지연(ms) | 최대 지연(ms) |
| 100 |
905.52ms
|
15ms
|
1065ms
|
| 200 |
792.62ms
|
33ms
|
1323ms
|
| 300 |
1134.25ms
|
55ms
|
1712ms
|
| 400 |
1360.95ms
|
41ms
|
2298ms
|
300명까지는 최대 지연이 2초 이내였고, 400명부터는 최대 지연이 약 2.3초로 넘어가면서 요구 조건을 초과했다.
이번 테스트를 통해, 우리가 설계한 시스템은 한 채팅방당 약 300명까지 안정적인 메시지 수신 지연을 보장할 수 있으며, 400명 이상부터는 성능 튜닝이 필요할 수 있다는 것을 확인할 수 있었다.
오토스케링을 CPU 사용량으로 설정해두었는데, 지연 시간을 기준으로 커스텀 매트릭을 만들어 지연이 2초가 넘어가면 수평확장되게 설계해도 좋을 것 같다는 생각을 했다.
마무리
처음 아키텍처를 설계할 때는 모르는 게 너무 많아서 너무 힘들었다...ㅜㅜ
설계만 끝나면 구현은 쉽게 끝날 줄 알았는데, 실제로는 그렇지 않았다. 연동을 시작하면서 설정을 빠뜨린 부분, 아키텍처에서 놓친 요소들,
그리고 예상치 못한 오류들이 줄줄이 터져 나왔다.
그때마다 문제를 하나씩 찾아가며 해결하는 과정을 반복하면서, 몰랐던 걸 많이 배울 수 있었다.
사실 공부를 먼저하고 설계를 했어야했는데, 이게 반대로 된 기분이었다ㅎㅎ...
또, 이번 프로젝트를 진행하면서 인프라 설계부터 구축, 그리고 서비스 연동까지 직접 해보며 컴퓨터 네트워크와 정보통신공학 수업에서 배웠던 내용들이 계속 떠올랐다.
네트워크 지식이 정말 중요하다는 걸 느꼈고..., 그동안 프론트엔드만 하며 잘 몰랐던 서버와 인프라 영역도 이번 기회에 접하고 배울 수 있어서 좋았다.
또, 항상 연동 중 서버 쪽에서 문제가 생기면 이유도 모르고 기다리기만 했었는데, 이번엔 직접 EC2에 접속해서 로그도 확인하고, 문제를 추적하고 해결할 수 있었던 경험도 인상 깊었다.
물론 시간이 부족해서 아키텍처를 설계하거나 서비스들을 깊이 있게 공부하지는 못했지만, 이번 경험을 계기로 내가 사용한 AWS 서비스들과 그 구조에 대해 더 정리하고, 앞으로 더 공부해나가야겠다는 생각이 들었다.
'Infra' 카테고리의 다른 글
| 대규모 채팅 시스템 설계하기 2 : EC2 + WebSocket 기반 최종 아키텍처 (0) | 2025.05.20 |
|---|---|
| 대규모 채팅 시스템 설계하기 1: 실시간 통신 방식 (0) | 2025.05.12 |
| FastAPI EC2에 배포하기 (0) | 2025.05.07 |
| RDS & Amazon DynamoDB (0) | 2025.04.02 |
| Storage - S3 & CloudFront (0) | 2025.03.29 |