요즘 정말 많은 벽을 넘고 있다. 일도 하고 숙제도 많고 공부할 것도 많고.. 몰려오는 지식의 양이 너무 많아서 한달 정도 블로그 글을 쓸 엄두도 내지 못했다. 써야 할 글이 넘쳐나지만 이럴 수록 더 정제를 잘 하고 내걸로 만들어서 좋은 글을 올려야겠다. 오늘은 쿠버네티스를 처음 다룰 때 가장 헷갈리는 부분 중 하나인 외부 사용자의 요청이 어떻게 클러스터 안의 Pod까지 도달하는지에 대해 알아볼 것이다. Pod는 기본적으로 클러스터 내부 네트워크에 존재한다. 사용자가 브라우저에서 https://hskhsmm.com으로 요청을 보냈다고 해서 그 요청이 곧바로 Pod로 들어가는 것은 아니다. 중간에 DNS, 로드밸런서, 쿠버네티스 Service, Ingress 또는 Gateway 같은 여러 계층이 관여한..
전체 글
IT, etc기존에 수행중인 프로젝트의 배포는 Github Actions와 CodeDeploy를 기반으로 동작한다. Github Actions는 Docker 이미지를 빌드해서 ECR에 push하고 배포에 사용할 이미지 주소를 SSM Parameter Store에 저장한 뒤, appspec.yml과 배포 스크립트를 S3 bundle로 묶어 CodeDeploy 배포를 생성한다. CodeDeploy는 Auto Scaling Group에 속한 애플리케이션 EC2 인스턴스에 배포 bundle을 전달하고, appspec.yml에 정의된 lifecycle hook을 순서대로 실행한다. appspec.yml BeforeInstall -> deploy/scripts/beforeInstall.sh AfterInstall ..
이 프로젝트는 단순한 CRUD 서비스가 아니라, 대규모 이벤트성 트래픽을 처리하는 캠페인 오케스트레이션 시스템이다. Kafka 3 broker, Redis/Valkey Cluster, RDS MySQL, Auto Scaling Group, ALB, CodeDeploy, Prometheus/Grafana 기반 모니터링까지 포함해 실제 운영 환경에 가까운 구조로 AWS 인프라를 구성했다. 문제는 비용이었다. 개인 프로젝트에서 운영형 인프라를 계속 켜두면 비용 부담이 꽤 커진다. 특히 다음 리소스들은 테스트하지 않는 시간에도 계속 비용이 발생한다. - RDS MySQL- ElastiCache Redis/Valkey Cluster- Kafka EC2 3대- monitoring 및 terraform-mcp EC2..
대규모 서비스 환경에선 배포는 가끔 하는 일이 아니라 매일 수십, 수백 번 반복되는 운영 활동이 된다. 특히 금융권 대고객 서비스처럼 장애 영향이 큰 환경에선 빠르게 배포하는 것만큼이나 장애 반경을 작게 만들고, 문제가 생기면 즉시 되돌리는 구조가 중요하다. 큰 그림부터 보자. Kubernetes, 줄여서 K8s는 컨테이너로 실행되는 애플리케이션을 관리하는 플랫폼이다. 여기서 애플리케이션은 보통 Pod라는 단위로 뜬다. Pod는 컨테이너를 감싸는 가장 작은 실행 단위라고 보면 된다. 예를 들어서 account-service라는 서비스가 있으면 같은 역할을 하는 Pod가 여러 개 떠 있고 트래픽이 그 Pod들로 분산된다. 근데 사용자는 Pod에 직접 접근하지 않는다. 중간에 Service라는 쿠버네티스..
이전 글에서는 Prometheus와 Grafana를 어떻게 구축하는지에 집중했다. 당시에는 Spring Boot 애플리케이션에 Actuator를 붙이고, Prometheus가 지표를 수집하고, Grafana에서 대시보드를 구성하는 흐름을 사용 방법 중심으로 정리했다. 하지만 실제 부하 테스트를 반복하면서 느낀 점은 모니터링 도구를 설치하는 것보다 무엇을 지표로 볼 것인지 정의하는 일이 훨씬 중요하다는 것이었다. 처음에는 API TPS, p95, p99, 5xx, CPU 같은 기본 지표만 보면 충분하다고 생각했다. 하지만 선착순 캠페인 시스템을 Redis-first 비동기 구조로 바꾸고 나니, API 응답 지표만으로는 시스템의 실제 상태를 판단하기 어려웠다. API는 사용자에게 202 Accepted를 빠..
대규모 선착순 캠페인 시스템을 설계하면서 중요하게 고민했던 부분 중 하나는 한 명의 유저가 재고를 여러 개 가져가는 중복 참여를 어떻게 완벽하게 막을지였다. 초기엔 API 진입점에 있는 RateLimit의 TTL을 영구적으로 설정하면 한 번 들어온 유저를 평생 막을 수 있으니까 가장 효율적이지 않을까 생각도 했다. 그러나 분산 환경은 그렇게 단순하지 않다. 동시성과 비즈니스 로직의 특성을 고려해보면 RateLimit 하나만으론 데이터 정합성을 지킬 수 없다는 결론을 내렸다. 3가지 시나리오를 통해 설명해보겠다. 1. 억울한 탈락자 발생 가장 큰 문제는 성공하기 전에 영원히 갇히는 유저가 생긴다는 점이다. RateLimit은 시스템을 보호하기 위해서 비즈니스 로직이 실행되기 전에 가장 바깥쪽에서 트래픽을..
(김영한 강사님의 스프링 DB 2편 강의 내용을 기반으로 학습하며 실무 아키텍처 관점을 추가하여 정리한 글입니다.) 비즈니스 로직을 구현하다 보면 이런 요구사항을 만난다. "회원가입은 메인 로직이니 실패하면 안 되고, 로그 저장이나 알림 발송 같은 부가 기능은 실패하더라도 회원가입 자체는 성공해야 한다." 너무나 당연하고 간단한 기획 같아 보인다. 개발자는 자신 있게 부가 기능 호출부를 try-catch로 감싸 예외를 복구하도록 코드를 작성한다. 하지만 테스트를 돌리거나 운영 서버에 배포하는 순간 다음과 같은 예외를 마주하며 회원가입까지 통째로 롤백되어 버린다.org.springframework.transaction.UnexpectedRollbackException: Transaction silently..
이전 글에서 ASG 2 대 도입과 배치 INSERT 최적화를 통해 100 만 건 정합성을 완벽하게 달성했다. 하지만 더 높은 부하에서 시스템이 버텨주는지 검증하고 싶었다. 그 과정에서 예상치 못한 구조적 결함을 발견했고 근본 원인을 추적해 해결하는 과정을 기록한다. 또한 시스템 완성에 대해 내가 생각한 점들을 함께 풀어보겠다. 12차 테스트 - ASG 3대, 재고 130만 100만 정합성을 달성했으나 마지막으로 확인하고 싶은 것이 있었다. 11차에서 앱 CPU가 최대 90%에 달했다. 인스턴스 2대가 한계에 가까웠다는 뜻이다. CPU가 포화되면 요청이 스레드 풀에서 대기하고 응답시간이 늘어난다. 인스턴스를 추가하면 ALB가 트래픽을 분산하므로 각 인스턴스의 CPU 부하가 줄고, 응답시간이 짧아져 TPS..