1. 서론 (내.돈.내.산)
올해 2024년은 개발 역량 성장 부분에서 의미있는 한 해가 아니었나 싶다. 그동안 다양한 도서 / 강의 / 교육 등을 활용했었고, 5년간 자기개발에 약 450만원을 투자했다는 사실을 최근에 정리하면서 알게 되었다 (교육비 지원하는 회사없는가..)
자기 개발하면서 느낀거지만 "설명도 잘하고 전문성도 갖춘 전문가"도 있는 반면, "성의도 없고 광고에 속았다고 생각했던 강의"도 있었다. 그 중에서 올해 도움 받았던 강의가 하나 있었고, 그게 마침 패스트 캠퍼스 강의라서 연말 후기 이벤트 참여하게 되었다 (그렇다 이게 다 무료 쿠폰을 위한 포스팅이다)
요즘 같이 정보가 넘쳐나는 시대에 이 강의를 선택해야 할지 망설이는 개발자가 있다면 이 후기가 참고가 되길 바라며 포스팅합니다
*이 강의를 통해 배울 수 있는 것
① docker-compose 활용한 인프라 구성
② 동시성 제어 (분산 락, 비관적 락, synchronized)
③ 캐시 (로컬, 글로벌)
④ 부하테스트와 모니터링
⑤ 지표 분석 후 성능 개선 경험
*이 강의를 통해 배울 수 없는 것
① 테스트에 대한 세부적인 지식
② VPC, Subnet 네트워크 구성 등
만약 다 아는 내용이면 뒤로가기를 누르는게 서로에게 피곤하지 않고 좋을거 같습니다
2. 부하 테스트와 모니터링을 통한 점진적 성능 개선 과정
"네고왕 선착순 쿠폰 시스템"을 만들어 가던 과정을 단계/해결로 구분하여 정리하였다
초기 구성
- 서버1, DB 1대
- 부하 테스트는 Locust, 모니터링은 prometheus와 grafana 사용 (*docker-compose로 구성)
단계1. 동시성 이슈
선착순 쿠폰 최대 수량(500개) 설정하고 부하 테스트 수행할 경우 쿠폰 발급 내역(coupon_issues)이 최대 수량을 초과(1033개)하는 현상을 확인할 수 있었다
해결1.
이를 해결하기 위해 잘 알려진 아래 세가지 방식을 순차적으로 적용하여 결과를 확인하였다
① /v1/issue : synchronized 키워드 방식(코드)
② /v2/issue : Redisson 분산 락 방식(코드)
③ /v3/issue : PEMISTIC LOCK(비관적 락) 방식(코드)
결과적으로 쿠폰 발급 수량은 500개로 정확히 기록된 걸 확인할 수 있었고, 각 방식별로 RPS 성능 차이를 확인할 수 있었다
여기서 비관적 락(/v3/issue)을 사용하는 경우 RPS가 가장 높게 나오나, MySQL CPU 과부하가 확인되었다
단계2. MySQL 과부하
N명의 사용자가 쿠폰 발급 요청할 때 1 ~ 4번 과정이 트랜잭션 안에서 수행되다 보니 MySQL CPU 과부하가 발생하는 걸로 생각되었다 (부하 테스트도 데스크탑에서 함께 돌리다보니 잡음도 있지 않았나 싶음)
해결2. 모듈 분리, Redis 인터페이스 의존
- 쿠폰 발급 요청 서버(coupon-api)와 쿠폰 발급 처리 서버(coupon-consumer) 분리
- 동시성 제어는 Redission 분산락 사용
- 대기열 큐를 인터페이스로 발급 요청/처리 과정을 분리하여 사용자 트래픽 대응 및 MySQL 트랜잭션 제어
- Redis 자료구조 String, Set, List 사용하여 쿠폰 발급 요청 처리
리팩토링 통해 다음과 같은 결과를 확인할 수 있었다
① 모듈 분리하고 Redis 의존함으로써 결합도를 낮추고, 각 모듈의 책임과 역할이 명확해짐 (응집도가 높아짐, DIP 원칙)
② Redis 자료구조 활용하여 DB 네트워크 요청 횟수를 줄여 부하 감소됨
- String : 쿠폰 정책 캐싱 (coupons 테이블)
- Set : 쿠폰 발급 요청 중복 방지, {couponId, userId}를 저장
- List : 쿠폰 발급 요청 대기열 (Queue)
③ 부하 테스트시 CPU 부하가 5 ~ 9% 유지 확인
단계3. RPS 성능 개선
모듈 분리와 캐시 도입을 통해 과부하 문제를 해결할 수 있었지만, RPS는 1,000 ~ 1,500 로 목표 RPS를 만족하지 못했다
목표 RPS = 5,000 이상
원인 분석을 할 때 쿠폰 발급 요청 시나리오에서 대부분의 RedisTemplate 요청은 O(1)의 시간 복잡도를 가지기 때문에 크게 문제가 되진 않는다고 생각이 들었다
*쿠폰 발급 요청 시나리오
① 쿠폰 정책 조회 (Coupon) 및 유효기간 확인
② 쿠폰 중복 발급 요청 여부 확인 (SISMEMBER)
③ 수량 조회 (SCARD) 및 발급 가능 여부 검증
④ 요청 추가 (SADD)
⑤ 쿠폰 발급 대기열 추가 (RPUSH)
문제 원인은 분산락 획득/해제 과정에서 있었는데, 코드를 지우고 테스트 했을 때 지표에서 명확한 차이를 보였다
분산락 사용하는 경우 | 분산락 사용하지 않는 경우 | |
RPS (Request Per Second, 초당 요청 수) | 1,468.08 | 6,000 |
단순히 Redission 분산락 부분을 지우고 전후 확인했을 때 약 4배의 RPS 성능 차이가 확인이 되었다
하지만 락을 사용하지 않았기 때문에 동시성 이슈가 발생하였고, 이를 고려해서 리팩토링을 할 필요성이 있었다
해결3. Redis Script 사용
- 네트워크 비용 감소 : 한번에 쿠폰 발급 요청 Validation과 대기열 큐 저장 수행
- 동시성 제어 : Redis는 싱글 스레드 동작하기 때문에 원자성 보장
기존 서비스 로직에서 분산 락 의존성을 지우고, 인프라 계층에서 Redis Script 요청 처리하도록 리팩토링하였다
그리고 Script 수행 결과 반환 값에 따라 예외 처리할 수 있도록 하였다
부하 테스트 결과 RPS는 약 5.4배 평균 응답 시간은 약 26.7배 향상된 걸 확인할 수 있었다.
- 가상 유저 수 총 1,000명/초당 200씩 증가
- /v1/asyncIssue : 분산락 사용
- /v2/asyncIssue : Redis Script 사용
단계4. 로컬 캐시 추가
이전 단계에서 부하 테스트와 모니터링을 통해 성능 개선 결과를 지표를 통해 명확히 확인할 수 있었다. 여기서 더 나아가 글로벌 캐시(Redis)와 로컬 캐시(caffeine)를 도입하여 쿠폰 정책 캐시에 대한 네트워크 통신 비용을 더 줄이도록 리팩토링하였다
coupon-api 서버에 쿠폰 발급 요청 서비스 로직을 리팩토링 했을 때 다음 두 가지 효과가 있었다
① 로컬 캐시에서 쿠폰 정책 캐싱하여 글로벌 캐시에 대한 네트워크 비용 감소
② CouponRedisEntity(캐시 객체)에서 발급 수량 유효성 검사하기 때문에 문제 발생시 redis script 로직을 실행 X
이때 쿠폰 발급 요청이 정상 처리되면 coupon-consumer 서버에서는 다음 절차를 수행한다
① 대기열 큐에 있는 데이터 가져와 쿠폰 발급 처리 후 캐시 갱신 이벤트 발행
② @TransactionalEventListener 에서 redis와 로컬 캐시 갱신
③ coupon-api 서버에서는 쿠폰 발급 요청시 갱신된 CouponRedisEntity(캐시 객체) 사용하여 유효성 검사
(이하 반복)
*부하 테스트 결과 (가상 유저 수 총 1,000명/초당 200씩 증가) 이전 대비 RPS 성능 약 1.33배 향상되었다
3. 웹 시퀀스 다이어그램
https://www.websequencediagrams.com/
4. AWS 인프라 구성 및 최종 결과
지금까지의 인프라 구성을 AWS로 옮겼다. *강의에서도 AWS 인프라 설정을 다루기는 하나 VPC, 서브넷 등 네트워크 설정은 다루지 않기 때문에 NHN Cloud나 AWS 공식 유튜브 채널 활용해서 직접 구성을 했다 (아래 포스팅 링크 참고)
기술 스택
- Infra : AWS EC2, RDS, ElasticCache, VPC, Subnet
- Server : Java 17, Spring Boot 3.3.3, JPA, QueryDsl
- Database : MySQL 8.0, H2, Redis
- Monitoring : AWS CloudWatch, Spring Actuator, Prometheus, Grafana
- Etc : JUnit5, Locust, Gradle 8.1, Docker, SonarQube, Git, Postman
인스턴스 사양
참고. 전반적인 AWS 인프라 구성 과정
https://dev-ljw1126.tistory.com/462
*부하 테스트 실행
- 가상 유저 수 : 총 5,000 (초당 500씩 증가)
- 테스트 시간 : 30s (*AWS 비용 예측 어려워, 짧게 끝냄)
- 선착순 쿠폰 수량 : 500개
- 목표 RPS : 5,000
- *결과: RPS(Requests Per Second) 5107.21로 목표치 달성
AWS 비용 예측 되지 않아 짧게 했는데, 다음달에 보니 0.75$ 비용 청구되었다
크레딧 관련해서 학생이거나 결제하기 어려운 상황이라면 AWS에서 주기적으로 운영하는 세미나, 교육 프로그램을 활용하는 것을 추천한다. 설문 조사까지 완료하면 아래와 같이 기간제 크레딧 쿠폰을 등록하여 사용할 수 있다
로컬(집)에서 부하 테스트를 진행하다보니 응답속도가 길게 잡힌 것으로 보인다. 같은 서브넷에 부하 테스트용 인스턴스를 생성한다면 응답시간 통계가 개선될 것으로 생각한다
*데이터 베이스 확인
쿠폰 발급 수(issued_quantity)와 쿠폰 발급 내역(coupon_issues)이 500개 확인
쿠폰 발급이 끝났을 때 쿠폰 정책에 발급 가능 여부 속성(avaiableIssueQuantity : false)이 의도대로 갱신되어 있음을 확인
*모니터링 확인
AWS CloudWatch
- redis cpu 2.76%, mysql cpu 5% 미만으로 병목현상이나 성능 문제는 없는 것으로 보임
→ *redis와 mysql은 더 많은 트래픽을 받을 수 있다 - redis network in과 mysql 쓰기 작업(WriteThroughput)에서 피크를 형성하는 시점이 있음
→ 일시적 현상으로 크게 성능 영향 x
Prometheus, Grafana - 서버 모니터링
coupon-api 서버 | coupon-consumer 서버 | |
Heap Used | 23.63% | 5.55% |
Cpu Usage | 99.99% | 19% |
*최종 결론
현재 MySQL, Redis의 경우 부하를 더 받을 수 있는 반면, coupon-api 서버의 CPU 사용량이 거의 100% 이므로 scale-out 한다면 더 많은 트래픽 대응 기대할 수 있을 것으로 판단된다
*추가 반영
평소 궁금했던 오픈소스 툴을 도입하여 자동화하거나 성능 개선을 추가로 해보았다
① Testcontainers, Kafka 도입
- Redis 대기열 큐에 의존하는 형태라 이를 Kafka에 위임했을 때 얼마나 변하는지 궁금해서 도입
- 테스트 결과 RPS 6.97% 향상 확인
② k6, Apache JMeter 부하 테스트
③ 정적 코드 분석 도구 (SonarQube, SonarLint)
④ Rest API 문서 자동화 (Spring Rest Docs + Swagger UI)
⑤ Redis Replica / Sentinel / Cluster 구성, DB Replication(master-slave) / Ochestrator / SQLProxy 등
https://github.com/ljw1126/coupon-issue
5. 현재까지 느낀점
지금까지 하나의 프로젝트 속에서 동시성 제어, 부하 테스트와 모니터링, AWS 인프라 구성 등 여러 내용을 다룰 수 있었다. 가끔 현업에서 토이 프로젝트에 대한 언급이 나올 때면 "일 하느라 바쁜데.."라며 항상 속으로 생각하고 미루며 살아왔었다. 하지만 이번 과정을 통해 다뤄 보지 못했던 지식이나 오픈소스 등을 활용해 볼 수 있었고, 실무에서도 활용할 수 있는 역량을 쌓을 수 있었다고 생각한다
특히, 이번 강의에서 모니터링 지표를 기반으로 문제 원인을 분석하고 개선해 가는 과정이 인상깊었다. 초기에 docker를 활용하여 인프라 환경 구성하고, 테스트 기반으로 요구사항 기능 구현, 그리고 부하 테스트와 모니터링을 통한 단계별 원인 분석 및 리팩토링하여 점진적으로 성능 개선하는 과정은 실무에서도 할 수 없었던 좋은 경험이었다
과거 세미나에서 "테스트와 모니터링 없는 환경은 재앙이나 다름없다"던 발표자의 말이 떠오른다
그 동안 혼자 자기 개발하면서 단위 테스트/도메인 설계/리팩토링/디자인 패턴/아키텍처 등 여러 지식을 학습을 해왔다. 그러나 실무에서 사용하기에 괴리감도 있고, 나의 역량도 부족했고, 그리고 지식 수준을 동일하게 맞추는 것도 어렵기 때문에 현실 타협할 수 밖에 없었다. 하지만 이번 경험을 계기로 타협을 해야 하는 부분과 하면 안되는 부분에 대한 경계(기준)이 세워질 수 있었다. 또한, 선택지가 늘었기 때문에 문제 상황에 부딪혔을때 내가 가진 무기 중에 상황에 맞는 적절한 도구를 잘 선택해서 문제 해결해가면 될 거라는 자신감이 든다
6. 앞으로의 목표
"잘 하고 싶다"
최근 존경하는 인프런의 이동욱 CTO께서 "쪽팔릴 수 있는 용기"에 대해 영상에서 말씀하신게 생각이 난다. 본인이 아는 지식이나 경험을 글이나 말로써 표현해봄으로서 모르는 부분을 찾을 수 있는 기회가 생기게 되고, 또 그걸 증명하기 위해 테스트를 만들고 객관적인 지표를 구해 보충하다보면 한 단계 성장할 수 있다는 내용으로 이해했다
참고 링크. https://youtube.com/shorts/pvbz-51MNiU?si=gHtKF_R1I6zdKbee
자기 개발에 몰입한지 1년 동안 도서 / 강의 / 교육 / 커뮤니티 등을 스스로 찾아 활용해왔다. 그리고 이 과정에서 알게 된 지식을 노션과 블로그에 정리하면서 학습 기록을 통해 성장할 수 있었다. 하지만 세미나 or 컨퍼런스 or 기술 블로그를 보다보면 "세상은 넓고 잘하는 사람은 많다"는게 체감되었고, 내가 아직 부족한 부분이 많다는 걸 느낀다. 그래도 뇌가 허락하는 한 앞으로 성장을 하기 위해 계속 노력해보려 한다. 그리고 최종적으로 이직을 성공적으로 하여, 가까운 미래에 좋은 영향력을 끼치는 개발자로서 사람들 앞에 언젠가 서보이고 싶다
(부디 저에게 원기옥을..)
7. 기대평
- 지식공유/커리어 관리 등 사람마다 목적이 다르고, 퀄리티 부분에서도 차이가 나기 때문에 남은 내용에 대해 많은 기대는 하지 않는다
- 다만, 힌트(ex.실무경험, 오픈소스, 노하우 등)가 있다면 관심사가 일치하거나 필요로 할 때 참고 용도로 활용하여 방향을 잡는데는 도움이 될 것으로 생각된다
이름모를 "개발자J"님 좋은 강의 찍어 주셔서 많은 참고와 도움이 되었습니다. 감사했습니다🙇🏻♂️
본 게시물은 패스트캠퍼스 후기 이벤트 참여를 위해 작성되었습니다
https://fastcampus.co.kr/event_online_review2024
'공부 > 기타' 카테고리의 다른 글
[Obsidian] AnuPpuccin 테마 - background image 설정이 안 보일 때 해결 방법 (1) | 2024.12.31 |
---|---|
[패스트캠퍼스] 완강 후기 - The Red:25개 백엔드 개발 필수 현업 예제를 통해 마스터하는 Java Stream (0) | 2023.12.12 |
sdkman으로 스프링부트 프로젝트 설치, h2 database 설치 및 접속 (0) | 2023.11.27 |
[GitHub] Copilot 체험판 구독 해지 (individual subscription trial) (0) | 2023.08.04 |
게임 지표 용어 정리 (0) | 2023.04.10 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!