프로젝트 기간 : 2023/08/16 ~ 2023/09/18
👑 스파르타 내일배움캠프 최종 프로젝트: 최우수 프로젝트 선정 👑
여행 상품 주문 서비스
- 대량의 트래픽이 발생할 것으로 예상되는 이벤트 여행 상품(초특가 타임딜) 페이지
- 대량의 여행 패키지 데이터(천만 건)에서 검색으로 필요한 여행패키지를 빠른 시간 내에 찾을 수 있게 한다.
Back
Front
Tool
Crawling
기술도입배경이 궁금하다면 이 링크를 클릭해주세요!
💻 기능 API
- 세션 인증/인가 - 멀티로그인 방지, 레디스로 세션 관리
- 여행상품 조회
- 여행상품 검색(필터 검색)
- 상품 구매
📟 대용량 트래픽 처리
- 가상사용자 1만명, 초당 약 1.9만 건 요청 처리
📚 상품 조회속도
- 여행상품 데이터 1,000만 건 누적 후
- 페이지네이션 상품 조회 속도 : 0.1초 이내
🔎 검색 속도
- 최초 검색 속도 : 0.1초 ~10초(데이터 차지량에 따른 차이 발생)
- 캐싱 이후 검색 속도 : 0.001초 이내
1. 💿 DB 최적화 - 여행 상품 데이터 조회 속도 개선
문제점 : 여행 상품 데이터 증가에 따른 메인 페이지 속도 저하
개선 : 커서 기반 페이지네이션 적용
Offset-based Pagination(개선 전) | Cursor-based Pagination(개선 후) |
---|---|
51초 소요 | 0.08초 소요 |
개선 결과 : 51초 -> 0.1초 이내로 조회 속도 개선
DB 최적화 - 여행 상품 데이터 조회 속도 개선 노션 링크
2. 📀 DB 최적화 - 여행 상품 검색 속도 개선
문제점 : LIKE문을 통한 검색 속도 지연 (약 10초)
개선 : Full-Text index(전문 검색) 사용
개선 결과 : 검색 키워드에 대해 조회 속도 개선. 단, 데이터량을 많이 차지할수록 FullText 인덱스의 성능이 저하됨
3. 🚀 레디스 캐싱
문제점 : 자주 검색되는 데이터에 좀 더 빠른 접근 필요
개선 방법 : 검색 기능에 Redis 캐싱 적용
캐싱 적용 전 | 캐싱 적용 후 |
---|---|
개선 결과 :
나트랑 검색
속도 11.69s -> 6.4ms
4. 🛣️ 부하 분산 - ELB
문제점 : 가상 사용자 1,000명에 대한 부하테스트를 통과하지 못함
개선 : ELB 도입으로 부하 분산
-
테스트 케이스
- PK ID 값을 사용하여 상품 상세 페이지 조회
- HTTP GET 요청
- AWS EC2 환경에서 테스트 (원격)
- timeout
3초
설정(3초 초과 시 error처리)
- 테스트 실행 환경
원격 서버(배포 서버) | nGrinder 서버 | |
---|---|---|
인스턴스 타입 | ec2.t3a.xlarge | ec2.m5zn.large |
CPU | 4 vCPUs | 2 vCPUs |
RAM | 16GB | 8GB |
Storage | 8 GiB, EBS, 범용 SSD(gp3) | 8 GiB, EBS, 범용 SSD(gp3) |
RDS(DB) | db.t3.xlarge(4 vCPUs, 16 GiB Mems) | x |
- 테스트 설정 : 15분간 1,000~10,000명의 가상 사용자로 상품 상세 페이지를 조회하는 GET요청 부하 테스트 수행
Vuser: 1,000 | Vuser: 10,000 |
---|---|
개선 결과 : 이전보다 짧은 timeout 제한에도 불구하고, 에러가 완전히 일어나지 않는 모습을 보이고 있다. 특히, Vuser를 10,000으로 설정했을 때의 TPS 그래프를 보면, 그래프는 전체적으로 우상향하고 있는 형태를 띄고 있다. 이는 부하 상황에서 트래픽을 더 효율적으로 처리했다는 의미로, ELB 사용을 통해 성능이 향상되었다고 해석할 수 있다.
groovy
언어로 작성- HTTP GET, POST 요청
- 테스트 코드 보러가기 → Tour-Ranger-Test-Runner
📚 1. SQL : 검색 시 사용자가 선택한 조건의 조합에 따른 쿼리 적용 방법 고민
문제: 여러 조건 중 일부 조건만 선택했을 시 어떻게 처리할 것인가?
검색 시 국가, 여행사, 출발 - 도착 날짜, 가격 조건을 추가
ex) 조건의 조합
조건 1개 : (국가), (여행사), (날짜), (가격) ⇒ 4
조건 2개 : (국가,여행사), (국가, 날짜), (국가, 가격), (여행사, 날짜), (여행사, 가격), (날짜, 가격) ⇒ 6
조건 3개 : (국가, 여행사, 날짜), (국가, 여행사, 가격), (국가, 날짜, 가격), (여행사, 날짜, 가격) ⇒ 4
조건 4개 : (국가, 여행사, 날짜, 가격) ⇒ 1
4 + 6 + 4 + 1 = 15
...
⇒ 다양한 경우의 수(15가지) 에 각각 해당하는 조건문과 쿼리를 모두 만들어 사용하는 것은 매우 비효율적이다.
해결: SQL 쿼리 내에서 아래와 같은 코드 기법 사용
SELECT *
FROM item i
WHERE MATCH(i.name) AGAINST(:search IN BOOLEAN MODE)
AND (COALESCE(:countries) IS NULL OR country IN (:countries))
AND (COALESCE(:travelAgencies) IS NULL OR travel_agency_id IN (:travelAgencies))
AND (:startDate IS NULL OR (DATE(departure_time) = :startDate AND DATE(arrival_time) = :endDate))
AND (:priceValue IS NULL OR ((:priceAbove = true AND :priceValue <= discount_price) OR (:priceAbove = false AND :priceValue >= discount_price)))
ORDER BY i.id DESC
조건값 IS NULL OR 컬럼 IN(조건값)
기법 이용
사용자가 조건을 선택하지 않았을 경우 - 프론트엔드로부터 NULL값을 전달 받아 조건을 TRUE로 만들어 해당 조건이 실행되지 않음
사용자가 조건을 선택한 경우 - 프론트엔드로부터 조건 값을 전달 받아 작성한 조건 쿼리를 수행
⇒ 하나의 쿼리로 모든 경우의 수를 처리
🔒 2. 동시성 제어 : 한정 상품 동시 주문에 대한 데이터 정확성 확보
문제: 여러 고객이 여행상품을 동시 구매 시, 팔린 아이템 개수 ≠ 구매된 아이템 개수 데이터 불일치 문제 발생
테스트 조건 : 1,000명이 동시 구매 요청, 수량 : 100개
남은 Item 수량 체크 | 구매 개수 체크 |
---|---|
expected: <0> but was: <6> Expected: 0 Actual: 6 |
expected: <100> but was <936> Expected: 100 Actual: 936 |
1000명이 상품 주문을 요청했을 때 아이템 현재 수량과 구매 개수의 예상 값이 다름. ⇒ 데이터 불일치
해결: 비관적 락 사용
-
데이터 일관성을 보장하는 데 효과적이고 코드가 간결해 관리하기 쉬운 비관적 락 사용
개선 결과: 동시 주문 요청에 대해 데이터 정확성 확보
🤖 3. 멀티스레드 사용으로 CPU 사용률(%) 개선
문제: 부하테스트를 진행하는 중이었음에도 CPU 사용률이 저조했다.
비관적 락을 사용하면 대기 시간이 길어진다. 그럼에도 테스트에서는 예상수치보다 한참 낮은 두 자릿 수의 TPS를 달성하였다.
원인을 찾기 위해 CloudWatch를 통한 모니터링을 진행하였고, 우선 (부하테스트로 인해서) CPU가 과열된 것은 아닌지부터 모니터링 해보았다. 하지만 CPU 사용률은 전혀 높지 않았고, 오히려 부하테스트를 진행하지 않을 때와 유사한 사용률을 보였다.
CloudWatch를 통해 CPU 사용률을 모니터링한 화면이다. VUser를 1000만큼 설정하여 테스트하였음에도 약 11.7% 라는 저조한 사용률을 보이고 있다.
관점을 바꿔서 Spring 애플리케이션이 CPU를 제대로 사용하지 못하고 있다고 판단하게 되었고, 이 부분에서 개선이 필요하다고 생각했다.
해결: 멀티스레드 사용 설정 및 멀티스레드 운용을 위한 EC2 인스턴스 스케일 업
- 필요한 서비스에 멀티스레드 사용 설정
@Async
@Async("taskExecutor")
public void purchaseItem(Long itemId, PurchaseRequestDto requestDto) {
/* 상품 구매 로직 */
}
- 멀티스레드 운용을 위한 EC2 인스턴스 스케일 업
- 기존에 사용하던 인스턴스 타입인 t2.medium은, 2 vCPU로, CPU 코어가 2개 밖에 없었다. CPU 코어 수가 적어서 멀티스레드를 사용하더라도 큰 효과가 없을 것이라 판단했고, CPU 코어가 4개이면서 가장 저렴한 t3a.xlarge를 선택하였다.
- 코어 : 스레드 = 1:1 비율로 맞추어, 기본 스레드는 4만큼 설정해주었다.
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4); // 기본 스레드 수
/* ... 그 외 설정 ... */
return executor;
}
}
멀티스레드 사용 전에 진행했던 테스트와 동일한 설정 (테스트 시간 제외*)으로 부하테스트를 실행하였다.
이번에는 약 31.8% 로, 이전보다 높은 사용률을 보였다. 스케일 업을 통해 EC2 인스턴스의 처리성능이 더 좋아졌음에도, 이전보다 더 높은 CPU 사용률을 보이고 있다.
🔑 4. 다중 서버에서 세션 관리
문제: 서비스의 특성 상 어뷰징의 위험이 있어 멀티 로그인 방지를 위하여 세션으로 인증, 인가 방식을 구현 하였는데, 웹 서비스에 걸리는 부하를 분산해주기 위해 서버를 늘리게 되었고, 이 과정에서 세션의 불일치 문제가 발생하였다.
-
세션 불일치 해결 방안
Sticky Session
: 세션이 유지되는 동안 동일한 서버에만 요청을 날리는 방식Session Clustering
: 모든 세션 저장소에 세션 객체를 복제하여 세션 불일치 문제를 해결하는 방식 ⇒ 트래픽이 더 증가할 가능성이 있기 때문에 짧은 시간 동안 트래픽을 견뎌내야 하는 우리의 서비스와는 맞지 않는다.세션 스토리지 분리 방식
: 세션 스토리지를 하나로 분리해 놓으면 트래픽을 증가 시키지 않고, 세션의 불일치 문제도 해결할 수 있다.
-
세션 스토리지에 알맞는 DB
In-Memory DB
: 세션은 존재하는지 여러 번 확인해야 하기 때문에 DB I/O를 많이 발생 시키고, timeout이 존재하는 데이터이기 때문에 In-Memory DB를 선택한다.Redis
: Redis와 Memcached 중 failover 기능을 더 구현하기 쉽고, READ 연산이 더 빠른 Redis를 사용한다.
해결: 세션 스토리지 분리 방식을 선택하고, 세션은 Redis로 관리해준다.
-
자세한 코드 내용(레디스로 세션 관리 전과 후에 대해 기록)
-
application.properties 설정 및 의존성 코드 추가
spring.session.redis.namespace=spring:session // application.properties
implementation 'org.springframework.session:spring-session-data-redis' // build.gradle 의존성 추가
- 인가처리된 세션이 레디스에 잘 저장되어 있음을 볼 수 있다.
🦈 5. AWS 배포 서버 중단 문제
문제: 배포 서버가 중단되는 문제
- AWS EC2 서버에 프로젝트를 실행시키고 일정 시간이 지나면 서버가 중단되는 문제
발생 원인
- AWS 프리티어 t2.micro 램은 1GB 정도 밖에 되지 않아, 프로젝트를 하나만 실행시켜도 메모리가 부족해 서버가 중단되었다.
해결: 리눅스 SWAP 메모리 설정을 통해 메모리 확보
- 스왑 공간을 너무 과도하게 늘리면 성능저하가 심해질 수 있으므로, 물리 메모리 1GB의 2배인 2GB 크기의 스왑메모리 확보
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
- 대기열 시스템을 생성하여 대량의 트래픽이 몰리는 것을 방지
- 트래픽이 몰릴 이벤트 여행 상품의 데이터를 미리 캐싱하고 캐싱된 데이터의 정보를 받아 빠른 처리 지원
- 검색 성능 향상을 위해 대용량 데이터 환경에서도 뛰어난 성능을 보이는 Elastic Search 도입
- 다나와 여행에서 크롤링
- 크롤링 코드 보러가기 → Tour-Ranger-Crawling GitHub Repository
이름 | 깃허브 |
---|---|
서예린 | https://github.com/yesrin |
조우진 | https://github.com/VVooJIN3 |
최정은 | https://github.com/jungeun5-choi |
표지수 | https://github.com/JisooPyo |