- 개발 기간 : 2023-07 ~ 2023-09 (MVP기능 구현 완료)
- 개발 기간 : 2023-10 ~ (트래픽 상황 대처 프로젝트 고도화)

Local Server | NCP (Docker) | AWS EC2 | Utility | Monitoring & Testing |
---|---|---|---|---|
SpringBoot 2.7.14 | MySQL 8.2.0 (M) | Jenkins 2.440.1 | MySQL Exporter (M) | Prometheus, Grafana |
Java 17 | MySQL 8.2.0 (S) | Tomcat 9.0.87 | MySQL Exporter (S) | VisualVM |
PostgreSQL 16.1 | Vault 1.15.6 | Flyway 8.4.4 | nGrinder | |
Redis 7.2.3 | Nginx 1.24.0 | SonarQube |
- 소스코드 관리 및 변경 감지
- Git Webhook
- SonarQube
- 자동화된 빌드 및 테스트
- Jenkins (AWS EC2)
- Build (.WAR file)
- Unit Test
- Jenkins (AWS EC2)
- 자동 배포
- Tomcat (AWS EC2)
- Nginx 무중단 배포 (예정)
- 데이터베이스 관리
- Docker (NCP)
- Reverse Proxy
- Load Balancing
- Caching
- Scale-Up & Scale-Out
- HashiCorp Vault
- PostgreSQL 검색 전용 DB로 역할 분배
- Full Text Search
- GIN INDEX
- tsvector, tsquery
- Full Text Search
- Master-Slave Replication으로 MySQL Read/Write 역할 분리
- Slave DB Scale-Out
- Round-Robin
- MHA Failover (예정)
- Prometheus로 Metric 수집
- MySQL Exporter
- Spring Actuator
- Grafana 활용하여 Prometheus Metric 시각화 처리
- Slave DB Scale-Out
- 멀티 쓰레드 동작 중 발생 가능 문제점 해결 방안
- 낙관 락(Optimistic Lock)
- 비관 락(Pessimistic Lock)
- 분산 락(Redisson, Distributed Lock)
- Redis Caching
- Write-Behind Caching
- Time-To-Live (TTL) Caching
- Redis zSet
GitHub | Git Webhook | Jenkins | SonarQube | WAR Deployment | Tomcat | Monitoring |
---|---|---|---|---|---|---|
Code Push | Webhook Trigger | Build & Test | Code Quality Analysis Report | Deploy WAR via HTTP | Unzip & Compile WAR | Grafana, VisualVM |
- Jenkins 활용
- 확장성: Jenkins는 Plugin을 통해 대부분 종류의 개발, 테스트, 배포 작업을 자동화할 수 있다. GitHub, GitLab과 같은 다양한 소스 코드 관리 도구와 통합할 수 있으며, Slack, 이메일 등을 통한 알림 설정 가능
- Trigger: Git Webhook이 Jenkins에 Push 알림
- Pipeline: Jenkins의 파이프라인은 Groovy 기반의 스크립트로 정의될 수 있으며, 빌드, 테스트, 배포 등의 작업을 세밀하게 제어할 수 있다.
- 메모리 부족 오류: Jenkins는 빌드 프로세스 중에 복잡한 프로젝트나 동시에 여러 빌드를 실행할 경우, 메모리 부족으로 인해 빌드 실패
(Free Tier → t2.small scale-up) - Jenkins Pipeline에서 Tomcat 서버에 접속하기 위한 쉘 스크립트 실행 시도 시,
Permission denied
오류 발생: Tomcat 서버의~/.ssh/authorized_keys
에 Tomcat rsa.pub 공개키 추가, Jenkins에 Tomcat 서버용 RSA 개인키를 Credential로 추가하고, 해당 CredentialID를 Pipeline에서 사용
- 확장성: Jenkins는 Plugin을 통해 대부분 종류의 개발, 테스트, 배포 작업을 자동화할 수 있다. GitHub, GitLab과 같은 다양한 소스 코드 관리 도구와 통합할 수 있으며, Slack, 이메일 등을 통한 알림 설정 가능
- Tomcat 활용
- 분리된 환경: WAR 파일을 외부 Tomcat에 배포하여 App과 서버 환경을 분리
- 자원 할당 최적화: 외부 Tomcat을 사용하면, 서버의 자원(CPU, 메모리 등) 할당과 관리 자유도 높음 (내장 Tomcat은 JVM 설정에 의존적)
- 로드 밸런싱 : 여러 외부 Tomcat 인스턴스를 운영함으로써 트래픽이 급증하는 상황에서도 안정적인 서비스를 제공하는 데 기여
- 보안: 외부 Tomcat 서버를 사용하면, 서버의 보안 설정을 App과 독립적으로 관리할 수 있다. (접근 제어, SSH/SSL 등을 App 변경 없이 수행가능)
- AWS → NCP에 설치된 Docker 안의 DB Container 접근 문제
-
JSch를 이용한 SSH 터널링: Spring Boot에서 JSch 라이브러리를 사용하여 NCP 서버에 SSH 접속 설정
-
포트 포워딩 설정: NCP 서버에서 Docker 컨테이너로 포트 포워딩을 설정하여, 특정 포트를 통해 DB 컨테이너에 접근
MySQL(Master) MySQL(Slave1) MySQL(Slave2) PostgreSQL Redis 3306:3306 3307:3306 3308:3306 5432:5432 6379:6379
-
- Tomcat WAR 파일 최대 업로드 크기 문제
- server.xml 수정: conf/server.xml 수정하여 <Connector> 태그 내의 maxPostSize 속성 값을 52428800(50MB) 에서 157286400(150MB) 로 변경 (배포 WAR file 80.1MB 용량 초과)
- Tomcat App 실행 중 자동 배포
- Tomcat 프로세스 확인: 배포 script는 실행 중인 Tomcat App의 PID 확인 (
TOMCAT_PID=$(ps -ef | grep tomcat | grep -v grep | awk '{print $2}')
) - 프로세스 종료: 실행 중인 Tomcat이 있을 경우
kill -15 $TOMCAT_PID
로 프로세스에게 종료 요청 후 프로세스가 완전히 종료될 때까지 대기
(kill -9
는 프로세스가 SIGTERM에 반응하지 않거나 강제 종료가 필요한 경우에만 사용) - Tomcat 재시작: 프로세스 종료 후,
./bin/startup.sh
를 실행하여 Tomcat를 재시작하여 새로운 배포 적용
- Tomcat 프로세스 확인: 배포 script는 실행 중인 Tomcat App의 PID 확인 (
- 분리된 환경: WAR 파일을 외부 Tomcat에 배포하여 App과 서버 환경을 분리
-
Reverse Proxy 활용
- Load Balancing: Nginx의 로드 밸런싱 알고리즘을 활용하여 톰캣 서버 간에 트래픽을 효율적으로 분산시켜 성능을 최적화할 수 있다고 판단
- Weighted Round Robin
- Tomcat-1 : t2.medium(2vCPU 4GB) weight=2
- Tomcat-2 : t2.small(1vCPU 2GB) weight=1
- Weighted Round Robin
- Caching: 정적 또는 동적 컨텐츠의 일부를 Nginx에서 캐싱함으로써, 반복적인 요청에 대해 빠른 응답 제공, 백엔드 서버의 부하 감소 및 응답 시간 단축
- 목표: 최대 1000명까지 안정적인 서버 운영
- 과정: nGrinder로 vUser 수 점진적으로 올리면서 Scale-Up과 Scale-Out의 스케일 조정
-
t2.micro(1vCPU 1GB) 단일 Tomcat 100명 Test 실행, Read Time Out 발생 → t2.small(1vCPU 2GB) Scale-Up
-
t2.small 단일 톰캣 400명 Test 실행, nGrinder CPU 70%, Tomcat CPU 65% 사용 → 1000명 Test 실행, nGrinder/Tomcat CPU Usage 100% 초과 Read Time Out 발생
-
t2.medium(2vCPU 4GB) 단일 Tomcat 1000명 Test 실행, Nginx의 CPU 사용량은 62%, Tomcat CPU 사용량은 130~140% 유지, 200% 모두 사용하지 못 함
- nGrinder가 WAS에 트래픽 부하를 걸지 못한다고 판단 → 사용자 수 2천명 고려하여 nGrinder 4vCPU 8GB Scale-Up
-
tomcat-2 (t2.small) 증설하여 로드 밸런싱 설정 (weight=2:1비율)
Tomcat 서버 2대 모두 CPU 사용량이 191%, 98%로 최대 사용량에 근접했고, Nginx CPU 사용량 역시 85%로 높은 사용량을 보이고 있다.
-
Scale 유지하여 2000명 Test 실행, vUser 대비 TPS가 기대치만큼 나오지 않음
- nGrinder CPU 사용량이 약 70%로, WAS 서버를 3대로 증설 또는 Tomcat-2의 Scale-Up 시 2000명도 충분히 트래픽을 버틸거라 판단
-
vUser 40 400 1000 1000 2000 Tomcat-1 t2.micro t2.small t2.medium t2.medium t2.medium Tomcat-2 X X X t2.small t2.small TPS 84.3 1637.4 2678.1 4116.6 3838.2
로드 밸런싱을 적용한 후, 빨간선을 기준으로 병목 현상이 감소하였으며, 이는 CPU의 최대 사용률 지속 시간이 늘어난 것으로 확인된다.
또한, 최대치까지 활용된 live thread 수치는 시스템이 높은 요청 처리량을 효율적으로 소화할 수 있음을 시사한다. Heap Size의 증감률과 낮은 GC activity는 현재 메모리 관리가 비교적 잘 이루어지고 있다고 판단- Weighted Round Robin 대신 Least Response Time Method 사용 시 성능 비교
- 지속적 Scale-Up의 한계
- Load Balancing: Nginx의 로드 밸런싱 알고리즘을 활용하여 톰캣 서버 간에 트래픽을 효율적으로 분산시켜 성능을 최적화할 수 있다고 판단
- SonarQube
- 코드 품질 개선: 개발 과정에서 자동으로 코드 스멜(Code Smells), 버그, 취약점 등을 식별하여 코드의 문제점을 인식하고 개선하여 리포팅할 수 있다.
- 팀워크와 코드 품질 문화 증진: 팀 내에서 코드 리뷰를 촉진하고, 모든 팀원이 코드 리포트를 볼 수 있어 코드 품질에 대한 인식을 높여 협업 증진
- Vault
- 안전한 비밀 관리: 민감한 정보 (API 토큰, DB 접속 정보, 비밀 키 등)를 암호화하여 저장하고, 권한에 따라 안전하게 접근 제어할 수 있다.
Vault로.yml
에 공개된 정보의 노출 리스크를 줄일 수 있다고 판단 - 중앙화된 비밀 관리: 모든 정보를 한 곳에서 관리함으로써, 설정 변경이 필요할 때마다 애플리케이션을 재배포해야 하는 불편함 해소 가능
- 안전한 비밀 관리: 민감한 정보 (API 토큰, DB 접속 정보, 비밀 키 등)를 암호화하여 저장하고, 권한에 따라 안전하게 접근 제어할 수 있다.
-
PostgreSQL 활용
- 검색 정확도: PostgreSQL의 tsvector는 텍스트를 토큰화하고, tsquery는 검색어를 토큰화하여 검색 정확도 향상, PostgreSQL의 어간 추출, 불용어 설정
- GIN 인덱스: GIN 인덱스는 토큰에 대한 포인터를 저장하여, 특정 토큰을 가진 데이터를 빠르게 찾을 수 있다.
- 다양한 RDBMS 활용: 확장 모듈과 SQL 표준 준수로 인한 높은 호환성 제공으로 시스템의 확장성 기대
- LIKE 연산자를 사용한 검색: 가장 기본적인 문자열 검색 방식으로 와일드카드 검색 수행.
Full Table Scan은 테이블의 모든 행을 검사하기 때문에 데이터 양이 증가함에 따라 성능이 선형적으로 감소 - ts_vector와 ts_query를 사용한 Full Text Search: tsvector는 텍스트를 '단어'로 분할하고, 이를 정규화시킴. 이 단어들은 GIN 인덱스에 포인터를 저장하여 쿼리 시 각 단어를 효율적으로 검색 tsquery는 검색 쿼리를 tsvector 형식으로 변환하여, 인덱스에서 빠르게 검색
구분 (MenuReview) 100,000개 1,000,000개 Full Table Scan 726 ms 11.927 sec Full Text Search 493 ms 4.264 sec 처리속도 비교 -233 ms -7.663 sec
- 사용자가 특정 메뉴의 리뷰를 keyword로 검색 (ex. 달콤한, 맛 없는, 푸짐한 양 ...)
- 검색 요청이 들어오면, 먼저 keyword를
plainto_tsquery
함수를 이용해tsquery
형식으로 변환. keyword는 공백 기준 단어로 분할되고, 각 단어는 정규화- 변환된
tsquery
를 사용하여,tsvector
칼럼에 저장된 리뷰 텍스트와 매칭(@@
),GIN 인덱스
를 활용하여 효율적인 검색 수행- 매칭된 리뷰들을 반환. Full Text Search는 keyword가 포함된 리뷰를 빠르게 찾아내므로, 사용자는 원하는 리뷰 정보를 즉시 응답받음
-
Master-Slave 구조 활용
- 동기 복제 방식: 동기 복제는 모든 노드에 데이터 변경이 반영되어야만 트랜잭션이 커밋되는 방식이라 정합성을 보장하지만 전체 시스템의 성능 저하 우려
- 비동기 복제 방식: Master 노드에서 변경된 데이터를 지연 없이 빠르게 Slave 노드로 복제한다.
실시간으로 대용량 읽기 작업이 많은 메뉴 리뷰는 비동기 복제가 적합하다 판단
- 데이터 안정성: Master DB는 Write 작업을 처리하고, Slave DB는 Read 작업을 처리함으로써 부하 분산이 가능해질거라 판단
- 데이터 확장성: Master DB에 문제가 발생한 경우, Slave DB를 Master로 승격시켜 서비스의 중단 없이 운영 (Failover & Failback)
- Slave로 MySQL 선택한 이유
- 일반적으로 Master-Slave 복제 방식은 같은 RDBMS 간에서만 가능
- 각 RDBMS가 고유의 데이터 저장 방식과 통신 프로토콜을 가지고 있기 때문
- 일반적으로 Master-Slave 복제 방식은 같은 RDBMS 간에서만 가능
- Slave로 PostgreSQL 사용하지 않은 이유
- MySQL에서 PostgreSQL로 데이터를 복제하려면 데이터 변환 및 동기화를 처리할 수 있는 도구가 필요하며, Debezium이나 Kafka Connect와 같은 CDC 기반의 도구를 사용
- 추가적 기술비용으로 인한 후순위 배치
- tsvector를 이용한 전문검색 시 한국어를 지원하지 않음
- MySQL에서 PostgreSQL로 데이터를 복제하려면 데이터 변환 및 동기화를 처리할 수 있는 도구가 필요하며, Debezium이나 Kafka Connect와 같은 CDC 기반의 도구를 사용
- Slave 서버 증설: 웹 서버의 트래픽 증가로 인해 Read 작업의 부하를 감당하기 어려워질 수 있다. Scale-Out으로 각 서버가 처리하는 트래픽을 줄여 성능 향상과 데이터를 분산 저장해 안정성을 높일 수 있다고 판단
- Round-Robin: 각 Slave 데이터 소스를 공평하게 사용하여, 특정 데이터 소스에 과도한 부하를 방지한다.
- Failover: Slave 서버는 최소 2대 이상이어야 하며, Master 서버에 장애가 발생했을 때, Slave 서버 중 하나를 새로운 Master로 승격시키고, 나머지 Slave 서버들이 새로운 Master를 참조할 수 있어야 한다. (예정)
-
- Network Traffic Monitoring
10만개 데이터 Write/Read 작업 시 Master-Slave를 통해 네트워크 트래픽 부하를 분산시켜서 읽기 작업을 효율적으로 처리하고 있는 것으로 판단
DB Write Master Read Slave Read Master 2.10 MB/s 260.36 kB/s 3.71 kB/s Slave 1.49 MB/s 8.96 kB/s 269.74 kB/s -
QPS Monitoring
Master Read Slave Read
Master-Slave 구조에서 Master와 Slave의 QPS가 각각 2.0과 3.2로 분산되었는데, Master는 쓰기 작업에 더 많은 부하를 갖고 있고, Slave는 읽기 작업에 더 많은 부하를 갖고 있다. 이를 통해 읽기 및 쓰기 작업이 적절하게 분산되었다고 보여진다.
QPS는 단순히 쿼리 수를 측정하는 지표이기 때문에, 실제 작업의 처리 시간, 응답 시간 등과 같은 다른 요소들을 고려하지 못한다. 또한, 전체 interval에서의 평균 QPS를 측정하기 때문에 Write/Read 작업의 순간적인 QPS를 측정하기 어려움이 있다.
- Network Traffic Monitoring
-
Optimistic Lock 활용
-
Optimistic Lock 선택한 이유
- 대부분의 상황에서 실제로 동일한 리소스에 대한 동시 요청이 드물게 발생하고, 이런 상황에서는 Optimistic Locking이 더 효율적
- 낮은 비용으로 높은 동시성을 제공하며, 충돌 발생 시 재시도 로직을 통해 처리
-
Pessimistic Lock 사용하지 않은 이유
- 다중 사용자가 아닌 한 명의 사용자 이므로 충돌이 자주 발생하거나, 데이터 일관성을 보장이 중요한 작업이라 판단하지 않았음
-
Pessimistic Lock 활용
- Optimistic Lock과 성능 비교 시 비관적 락 우위
- 사용자 수가 증가함에 따라 낙관적 락과 비관적 락 사이의 처리 속도 차이가 점점 더 벌어질 것으로 예상
-
Distributed Lock, Redisson 활용 Lettuce는 계속 락 획득을 시도하는 반면에 Redisson은 락 해제가 되었을 때 최소한의 시도를 하기 때문에 Redis의 부하를 줄여주게 된다.
구분 (Users) 100명 1000명 Optimistic Lock 6.105 sec 24.529 sec Pessimistic Lock 1.417 sec 7.526 sec 처리속도 비교 -4.69 sec -17.00 sec Distributed Lock 1.748 sec 8.955 sec
- 100명의 사용자가 예기치 못하게 동시에 같은 Menu(Beverage)를 주문
- 주문 당 해당 메뉴 주문 수량만큼 재고 감소
- Pessimistic Lock을 통해 주문 중 다른 사용자의 주문(Transaction) 접근 제한
- Thread 순차적으로 1번 ~ 100번 사용자 주문
재고 - 주문 수량 >= 0
일 경우 주문 완료재고 - 주문 수량 < 0
일 경우ExceptionHandler
예외처리
-
Pessimistic Lock 선택한 이유
- 주문 시스템에서는 동시에 여러 사용자가 같은 메뉴를 주문하는 경우, 그 메뉴의 재고 수량을 동시에 변경해야 하는 상황이 발생할 수 있다.
- 비관적 락을 사용하면 한 번에 하나의 트랜잭션만 해당 메뉴의 재고를 변경할 수 있기 때문에 충돌을 방지할 수 있다.
-
Optimistic Lock 사용하지 않은 이유
- 낙관적 락은 충돌이 비교적 드물게 발생하는 상황에 유용하다.
- 주문 시스템의 경우 동시에 여러 사용자가 같은 메뉴를 주문하는 상황이 자주 발생하므로, 낙관적 락을 사용하면 충돌로 인한 롤백이 빈번하게 발생하여 오버헤드가 발생할거라 판단
-
Distributed Lock 선택한 이유
- Redisson은 자신이 점유하고 있는 락을 해제할 때 Pub/Sub방식으로 채널에 메세지를 보내줌으로써 락을 획득해야 하는 쓰레드들에게 메세지를 전달
- 단일 DB 환경에서도 사용할 수 있지만 분산 락은 여러 노드에 걸쳐 있는 데이터에 대한 동시성을 제어할 수 있어 분산 환경 확장성 고려하여 테스트
-
나머지 쓰레드(사용자 별 주문 요청)들은 락이 해제될 때까지 대기 상태에 머무른다.
-
이 방식은 동시성 문제를 방지할 수 있지만, 대기 시간이 길어질 수 있다는 단점
-
최대 사용자는 몇 명까지인지 부하테스트 필요 (사용자가 늘어날수록 시간도 기하급수적 증가)
Users 응답시간 10명 564 ms 100명 1.417 sec 1000명 7.526 sec 1억명 ? sec
- Redis Caching 활용
- @Cacheable, Look-Aside Caching 전략
- 최신 메뉴 등록 기준 5Page 이하만 Caching 처리
- 유저들이 최신 메뉴를 우선적으로 볼 것이라 판단
-
Redis Caching 선택한 이유
- 높은 트래픽을 효율적으로 처리: 사용자가 전체 메뉴를 조회하는 경우, DB에 직접 접근하지 않고 Redis에 캐싱된 데이터를 사용하면, 응답 시간을 크게 단축시키고 DB에 가해지는 부하를 줄일 수 있다.
- 일관된 사용자 경험 제공: 메뉴 정보는 자주 변경되지 않는 데이터라고 판단했다. Redis Caching을 사용하면, 사용자가 매번 동일한 데이터를 조회할 때 일관된 정보를 빠르게 제공할 수 있다.
-
DB Lock 사용하지 않은 이유
- 단순히 데이터를 조회하는 경우 (예: 메뉴 조회)와 같이 데이터의 변경이 없는 상황에서는 DB Lock 없이 Redis Caching만으로도 충분히 빠른 응답 시간과 효율적인 서버 운영
Caching 적용 전
Caching 적용 후
아래 값은 작업 시작 시간 에서 종료 시간까지의 평균 값으로 산출 (nGrinder 1분 측정)
구 분 | TPS | 응답시간(ms) |
---|---|---|
레디스 캐싱 전략 사용 전 | 171.4 | 55.67 |
레디스 캐싱 전략 사용 후 | 339.2 | 28.15 |
속도 개선 증가 | 2.0 배 | 2.0 배 |
- (Caching Miss 시 응답시간 - Caching Hit 시 응답시간) / Caching Miss 시 응답시간 * 100% → 캐싱 적용 후 응답시간이 캐싱 적용 전의 약 49.4% 단축
- Redis zSet 활용
- 특정 메뉴를 조회하면 조회수가 1만큼 증가한다.
- 주문 시 주문 메뉴의 주문수를 1만큼 증가한다.
- 정렬 기준에 따라 인기 메뉴를 조회한다. (최상위 3개)
- 조회수가 가장 높은 순으로 메뉴 3개를 내림차순 정렬
- 조회수가 같을 경우 주문수가 가장 높은 순으로 내림차순 정렬
- 주문수가 같을 경우 key(menuTitle)를 사전순 정렬
- Redis zSet 선택한 이유
- 실시간 처리: Redis는 실시간으로 데이터를 처리한다. 메뉴의 조회수가 변경될 때마다 즉시 ZSET의 스코어를 업데이트할 수 있다.
- 정렬 기능: zSet은 스코어에 따라 자동으로 메뉴를 정렬한다. 조회수를 Score로 사용하면, 인기 메뉴를 스코어가 높은 순서로 쉽게 조회할 수 있다고 판단했다.
- 동시성 처리: Redis는 단일 쓰레드 모델을 사용하며, atomic operations를 지원한다. 따라서, 여러 사용자가 동시에 인기 메뉴를 조회하거나, 조회수를 업데이트하더라도 데이터의 일관성을 유지할 수 있다.
- Redis zSet은 하나의 스코어를 기준으로 정렬하는 것이 일반적이다. 하지만 주문량과 조회수와 같은 두 가지 지표를 모두 고려하는 것이 메뉴의 인기도를 판단하는 데 더욱 정확할 것이라 판단했다.
- 여러 지표를 조합하면, 단일 지표를 사용할 때보다 성능이 저하될 수 있다. 그러나 이런 성능 저하는 레디스를 통해 메뉴 정보를 캐싱함으로써 최소화할 수 있다.
- 성능 저하는 Redis에 메뉴의 주문량과 조회수를 모두 캐싱해 인기도를 계산했다.

- 가장 높은 조회수(5)를 기록한
빵1
최상위 1번에 위치 - 조회수 동점을 이룬
빵2
와빵3
중 주문량이 높은빵3
이 2번 위치 - 전체 메뉴 중 상위 2개를 제외한
빵2
가 그 다음 3번 위치

redis-cli → ranking 이름의 Sorted Set(ZSET)에서, Score(조회수)가 0에서 10 사이인 요소들을 내림차순 조회
- 일일 인기메뉴 기준
- 기본적인 로직은 Redis에서 제공하는 opsForZSet() 메서드는 Sorted Set 자료구조를 활용하여 데이터를 저장한다.
- 점수(score)를 기준으로 데이터의 순위 정보를 관리하므로, 인기 메뉴의 순위를 레디스에 저장하고 관리하는 데 적합하다.
- 이전 데이터를 RDB에서 가져와 캐싱하고, 당일 데이터를 Redis에 보관하며 검색할 때마다 score를 1씩 증가한다.
- 동점일 경우, 메뉴 조회시 캐싱된 메뉴 주문수량 내림차순 기준으로 인기메뉴를 정렬한다.
당일 데이터 Redis 사용법
- 매번 주문을 할 때마다 Redis에 zSetOperations의
ZINCRBY
명령어로score
증가 (Atomic Operation)- 오늘이 끝날 때(자정)에 RDB에
Write-Back
값 저장- Redis Data 비우기,
redisTemplate.delete("AllMenus::*");
스케쥴러를 사용해 자정(00:00:00)이 됐을 때 Write-Back Caching
@Scheduled(cron = "0 0 0 * * *")
public void refreshPopularMenusInRedis() {
ScanOptions options = ScanOptions.scanOptions().match("AllMenus::*").count(500).build();
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
try {
Cursor<byte[]> cursor = connection.scan(options);
while (cursor.hasNext()) {
String key = new String(cursor.next());
CompletableFuture.runAsync(() -> {
Optional.ofNullable((Menu) objectRedisTemplate.opsForValue().get(key))
...
}, threadPoolExecutor).exceptionally(ex ->
log.error("Failed to process key: {}", key, ex); ...)
} cursor.close();
}
} // 동기식: for-each loop {...})
구 분 | Sync | Async | Async+Scan |
---|---|---|---|
응답시간 | 3.73s | 263.16ms | 88.75ms |
성능개선 (Sync대비) | - | 14.2 배 | 42.0 배 |
Synchronized → Asynchronized
I/O 작업을 동기적으로 처리하면, 작업이 완료될 때까지 쓰레드가 대기 상태가 되어야 하므로, 쓰레드의 CPU 사용률이 낮아진다.
CompletableFuture
비동기 처리 사용 이유
- 효율성: 메인 쓰레드가 별도의 작업 쓰레드의 완료를 기다리지 않고 다음 작업을 계속 진행하여 쓰레드의 CPU 사용률을 높일 수 있다고 판단하여 적용
- 에러 처리: 비동기 처리가 실패한 경우 감지할 수 있게 예외처리 가능
ThreadPoolExecutor
사용 이유
- 커스텀 설정: ThreadPoolExecutor의 설정을 직접 관리함으로써, 어플리케이션의 특성에 맞게 ThreadPool의 동작 제어
- 공유 리소스 관리: commonPool에서 쓰레드를 과도하게 사용하여 시스템 전체의 성능이 저하되는 것을 방지하기 위해 특정 작업에 대해 별도의 ThreadPool을 사용
scan 명령어
사용 이유
- Blocking 최소화: Redis는 Single Thread 구조로 동작하고, keys 명령어는 모든 키를 찾을 때까지 Redis를 Blocking 한다. 이는 다른 클라이언트의 요청 처리가 지연될 수 있다.
scan 명령어
는 일정량(count)의 키만 반환하여 Timeout 발생 할 확률 낮춤
parallelStream()
을 사용하더라도 병렬 쓰레드는 I/O 작업 대기시간을 없앨 수 없기에 사용 X
- 매일 자정이 되면, Redis에서
AllMenus::
로 시작하는 모든 키를 찾는다. 이 키들은 인기 메뉴 데이터를 나타낸다. - 이 키들을 찾은 후, 각 키에 해당하는 값을 가져온다. 값은 메뉴 score로, 인기 메뉴의 정보를 담고 있다.
- 각 인기 메뉴에 대해 해당 메뉴의 제목을 기반으로 RDB에서 같은 메뉴를 찾는다. 동시에, Redis의 Sorted Set에서 해당 메뉴의 인기 점수(score)를 가져온다.
- 만약 RDB에서 메뉴를 찾고, 그 메뉴의 인기 점수를 Redis에서 성공적으로 가져왔다면, RDB의 메뉴 정보에 업데이트한다.
- RDB에서 해당 메뉴를 찾지 못하거나 인기 점수를 가져오지 못한 경우에는, Redis에서 가져온 인기 메뉴 정보를 그대로 RDB에 저장한다.
- 매일 자정이 되면, Redis에서
AllMenus::
로 시작하는 모든 키를 찾아 삭제하여 새로운 일일 데이터를 위한 공간을 만든다. - 모든 메뉴의 score는 0으로 초기화된다.
- 장점
- 자주 접근되는 데이터나 실시간성이 중요한 데이터를 Redis에 저장하면 전반적인 시스템 성능을 크게 향상
- Redis의 순위 정보는 검색 또는 주문 시마다 변경 가능
- 주기적으로 Redis의 데이터를 RDBMS에 백업(write-back)하는 과정으로 비상 상황 발생 시 RDBMS에서 데이터를 복구할 수 있다.
- TTL(Time-To-Live)로 오래된 데이터가 자동으로 삭제되면서 Redis의 메모리 사용량을 효율적으로 관리
- 자주 접근되는 데이터나 실시간성이 중요한 데이터를 Redis에 저장하면 전반적인 시스템 성능을 크게 향상
- 일일 인기메뉴는 write-back, TTL(1일)로 해결되지만, 주별, 월별 인기메뉴 조회는 어떻게 처리할까?
- 신규 회원 가입 시 제공되는 선착순 쿠폰 100장 이벤트를 진행
- Pessimistic Lock 활용