Releases: boostcampwm-2024/refactor-web03-CorinEE
릴리즈 1.02 - 커넥션 풀, 트랜잭션 범위 축소, 렌더링 최적화
개요
이번 릴리즈에서는 커넥션 풀 할당 문제를 해결하고 트랜잭션의 범위를 축소하여 커넥션 풀을 점유하는 시간을 줄여 성능을 개선하였고,
실시간으로 받아오는 SSE 데이터에 의하여 불필요한 리렌더링이 일어나는 것을 발견하고 이것을 개선하였습니다.
새로운 기능
- 거래 체결 시간 98.86% 단축(초당 1000번의 요청 기준)
- 불필요한 리렌더링을. 또한 스크립트 실행 시간은 2212ms → 474ms로 약
약 78.6% 감소
(1738ms 단축)
개선 사항
불필요한 렌더링
기존 홈 페이지에 존재하던 코인 리스트는 서버로부터 SSE 데이터를 끊임 없이 전달 받으며 화면에 렌더링 한다. 이때, 가격이 변한 코인 뿐 아니라 가격이 변하지 않는 코인까지 리렌더링 되는 문제가 발생했다.
불필요한 리렌더링이 문제가 되는 이유
불필요한 리렌더링이 문제가 되는 이유
불필요한 리렌더링은 변경이 없는 컴포넌트까지 불필요하게 리렌더링되어 CPU/메모리 자원이 낭비된다. 즉 우리가 렌더링하는 코인 목록이 많으면 많을수록 성능 저하가 심각해진다. 특히 모바일 환경에서는 이러한 성능 저하가 배터리 소모와 직결되며, 사용자들이 실시간으로 코인 가격을 모니터링하는 데 있어 중요한 UX 저하 요인이 된다.
문제 원인 파악
문제의 원인을 파악하기 위해 컴포넌트의 구조를 도식화해봤다.
CoinList
컴포넌트를 들여다 보면 다음과 같이 동작한다
CoinList
내부 sseData는 useSSETicker
훅을 통해 실시간으로 받아오는 코인의 시세 가격 데이터다. 이 훅은 SSE 연결을 통해 모든 코인의 가격 정보를 한 번에 받아와 상태를 업데이트한다. 이로 인해 단 하나의 코인 가격만 변경되어도 전체 상태가 업데이트된다.
리액트는 부모의 컴포넌트가 리렌더링 되면 자식 컴포넌트가 함께 리렌더링되는 특성이 있다. 이는 리액트의 기본적인 렌더링 최적화 전략으로, 별도의 최적화 처리를 하지 않으면 부모 컴포넌트의 상태 변화가 모든 자식 컴포넌트의 리렌더링을 유발한다.
즉 각 코인 컴포넌트들은 부모 컴포넌트인 CoinList의 상태가 변경되어 자신의 가격 정보와 상관없이 리렌더링 되었던 것이다.
문제 해결
zustand의 전역 상태를 활용해 Coin 컴포넌트가 자신이 필요한 데이터만 구독하는 방식으로 해결해보기로 시도했다. 기존 Coin 컴포넌트는 자신의 코인 정보 외 모든 코인의 시세 가격 데이터를 전달 받고 있어 zunstand를 통해 필요한 데이터만 구독을 하면 위의 불필요한 리렌더링 문제가 해결될 것이라 기대했다. zustand의 selector 기능을 활용하면 각 Coin 컴포넌트가 자신의 market id에 해당하는 데이터만 구독할 수 있어, 다른 코인의 가격 변화에 영향을 받지 않게 된다.
다음과 같이 zustand를 사용해 코인의 시세 정보를 담는 전역 상태를 만들어주었다.
이후 SSEProvider
를 통해 Coin 컴포넌트에서 자신의 market 정보만 구독하여 자신의 정보만 받아오도록 하였다.
결과
✅ 개선 전
✅ 개선 후
불필요한 리렌더링을 줄여 개선 전 대비 CPU 사용량을 개선할 수 있었다. 또한 스크립트 실행 시간은 2212ms → 474ms로 약 약 78.6% 감소
(1738ms 단축)의 성능 개선을 이루었다.
커넥션 풀
문제 1: 잘못된 waitTransaction
함수 사용
- 동시에 여러 미체결 데이터를 생성할 때의 트랜잭션을 방지하기 위해
waitForTransaction
이라는 함수를 통해 동기적으로 실행하는 코드를 구현함. - 문제점: 불필요한 대기로 인해 비동기 작업의 성능을 저해.
- 해결 방법:
waitTransaction
함수 제거 및 코드 간소화.
문제 2: 커넥션 풀 할당 문제
- 그러나 동시 10개 요청 시 무한 대기 현상 발생함
- 원인은 TypeORM의 기본 커넥션 풀 설정이 10개여서 발생한 문제.
- 트랜잭션으로 묶여있지 않은
validateUserAccount
가 트랜잭션 커넥션 대신 새로운 커넥션 요청 → 커넥션 부족으로 대기 상태 발생.
-
개선점: 트랜잭션 내에서 동일한 커넥션(queryRunner)을 사용하도록 수정.
→ 불필요한 커넥션 요청 제거 및 대기 시간 단축.
테스트 결과
조건 1: 단일 서버
- 1000명 동시 매수 요청 (분할 체결, 한 번에 체결 고려하지 않음)
- 커넥션 풀 리미트: 10개
요청 처리 시간 | 최소 | 최대 |
---|---|---|
단일 서버 | 1초 | 69초 |
조건 2: 로컬 서버 2개
- 1000명 동시 매수 요청
- 서버 2개 → 커넥션 풀 두 배로 증가
요청 처리 시간 | 최소 | 최대 |
---|---|---|
로컬 서버 2개 | 1초 | 15초 |
서버가 두개인 경우 그만큼 커넥션 풀도 두 배로 할당되어 성능이 개선되는 것으로 추측
트랜잭션, 쿼리분석
개요
앞서 수행한 성능 개선 작업을 통해 스레드 및 프로세스의 문제가 아닌, 로직 자체에 문제가 있음을 확인하였습니다. 이에 의심되는 부분을 선별하여 분석을 진행하였습니다.
분석 대상
- 슬로우 쿼리의 존재 여부
- 트랜잭션 범위 축소
문제 1: 슬로우 쿼리 분석
기존 매수 로직에서는 두 가지 트랜잭션을 사용하고 있었습니다.
- 매수 등록: 4개의 쿼리
- 매수 체결: 9개의 쿼리
부하 테스트를 위해 데이터베이스에 1,000개의 데이터를 채운 후 각 쿼리를 개별적으로 분석한 결과, 모든 쿼리가 정상적으로 작동함을 확인하였습니다.
참고자료
문제 2: 트랜잭션의 범위 축소
트랜잭션이 완료되기 전까지 커넥션 풀이 점유되므로, 커넥션 풀에 부하가 발생할 수 있음을 학습하였습니다. 이에 거래 체결 시 사용되는 트랜잭션의 범위를 축소하기로 하였습니다.
수정 전 트랜잭션
- 등록된 미체결 데이터 검색
- 거래 기록 테이블에 데이터 삽입
- 유저 계좌 정보 업데이트
- 유저 자산 정보 업데이트
- 미체결 정보 업데이트 또는 삭제
수정 후 트랜잭션
- 등록된 미체결 데이터 검색
- 거래 기록 테이블에 데이터 삽입 (비동기 처리)
- 유저 계좌 정보 업데이트 (비동기 처리)
- 유저 자산 정보 업데이트 (비동기 처리)
- 미체결 정보 업데이트 또는 삭제
트랜잭션 범위를 축소함으로써 주요 작업을 비동기적으로 처리하여 트랜잭션 내 커넥션 점유 시간을 줄였습니다. 예외 처리를 위한 보상 트랜잭션 구조는 추후 설계가 필요합니다.
테스트 결과
테스트 조건
- 사용자: 1,000명 동시 매수 요청
- 커넥션 풀 제한: 10개
수정 전
-
매수 요청부터 체결까지 소요 시간: 10초 ~ 40초
-
1,000건 매수 데이터 체결 총 소요 시간: 30초
수정 후
-
매수 요청부터 체결까지 소요 시간: 11초 ~ 25초
-
1,000건 매수 데이터 체결 총 소요 시간: 14초
총 1,000건 거래 체결 시간을 30초에서 14초로 약 50% 개선하였습니다.
문제 3: 커넥션 풀 증가
개요
트랜잭션 범위 축소 후 첫 거래 체결까지 11초가 소요되었습니다. 이는 거래 등록 시와 체결 시 모두 트랜잭션으로 인해 커넥션을 점유하여 자원이 부족해 발생한 문제로 판단되었습니다. 이에 커넥션 풀을 기존 10개에서 100개로 증가시켰습니다.
테스트 결과
테스트 조건
- 사용자: 1,000명 동시 매수 요청
- 커넥션 풀 제한: 100개
수정 후 커넥션 풀 증가
- 매수 요청부터 체결까지 소요 시간: 0.8초 ~ 2.6초
- 1,000건 매수 데이터 체결 총 소요 시간: 1.6초
커넥션 풀을 10개에서 100개로 증가시킴으로써, 전체 거래 체결 시간을 14초에서 1.6초로, 첫 거래 체결 시간을 11초에서 0.8초로 단축하였습니다.
추가정보
릴리즈 1.01 - WorkerThread, JMeter
개요
이번 릴리즈에서는 js의 싱글쓰레의 한계를 극복하기 위해 workerThread 및 cluster를 적용해보기 위해 테스트 및 개선을 진행하였습니다.
새로운 기능
- Trade모듈 workerThread 적용
- JMeter를 활용해 정확한 실제 성능 측정 비교
개선 사항(진행중)
동기적으로 실행되던 코드 수정
- Jmeter를 이용한 테스트시 생각보다 성능이 나오지않아 원인파악 필요
- 매수/매도를 처리하는 로직에서 for .. of를 사용하여 모든 미체결데이터를 동기적으로 처리하는 코드 수정
- 기존코드는
handler
에 I/O 작업이 포함되어 있어도 강제로 동기적으로 실행하는 방식으로, 이는 각 I/O 작업(또는 비동기 작업)이 완료될 때까지 대기한 후 다음 작업을 처리하는 구조로 동작하여 성능이 제대로 나오지 않음.
for (const trade of availableTrades) {
try{
const tradeDto = this.buildTradeDto(trade, coinLatestInfo, tradeType);
this.logger.debug(처리 중인 거래: tradeId=${tradeDto.tradeId});
await handler(tradeDto);
} catch (err) {
this.logger.error(
미체결 거래 처리 중 오류 발생: trade=${JSON.stringify(trade)}, error=${err.message},
err.stack,
);
}
await Promise.all(
availableTrades.map(async (trade) => {
try {
const tradeDto = this.buildTradeDto(trade, coinLatestInfo, tradeType);
this.logger.debug(`처리 중인 거래: tradeId=${tradeDto.tradeId}`);
await handler(tradeDto);
} catch (err) {
this.logger.error(
`미체결 거래 처리 중 오류 발생: trade=${JSON.stringify(trade)}, error=${err.message}`,
err.stack,
);
}
})
);
개선 결과
마지막 작업의 경우 70-120초 → 1-2초로 개선
(1~2초 걸리는 시간은 백그라운드에서 진행되는 체결 로직의 텀을 줄이면 더 개선가능할 것으로 예상)
쓰레드넘버, 거래등록 시간 - 거래체결 시간, 거래등록 시간
진행 중
- workerThread 적용 중
- cluster 적용 중
버그 수정
trade-ask-bid.service.ts
[CorinEE] 25996 2025-01-15 16:40:57 ERROR [TradeAskBidService] 미체결 거래 처리 중 오류 발생: trade={"tradeId":"2335","userId":"70","tradeType":"buy","tradeCurrency":"KRW","assetName":"ETH","price":"5000000","quantity":"1","createdAt":"Wed Jan 15 2025 16:36:39 GMT+0900 (대한민국 표준시)"},
error=Transaction is not started yet, start transaction before committing or rolling it back. - {"stack":["TransactionNotStartedError: Transaction is not started yet, start transaction before committing or rolling it back.\n at MysqlQueryRunner.rollbackTransaction (C:\Users\seonghyeon\coding\boostcamp\membership\project\refactor-web03-CorinEE\.yarn\virtual\typeorm-virtual-2696ddb21c\6\AppData\Local\Yarn\Berry\cache\typeorm-npm-0.3.20-3cdc45367a-10c0.zip\node_modules\typeorm\driver\src\driver\mysql\MysqlQueryRunner.ts:165:46)\n at BidService.executeTransaction (C:\Users\seonghyeon\coding\boostcamp\membership\project\refactor-web03-CorinEE\packages\server\src\trade\trade-ask-bid.service.ts:115:25)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at BidService.bidTradeService (C:\Users\seonghyeon\coding\boostcamp\membership\project\refactor-web03-CorinEE\packages\server\src\trade\trade-bid.service.ts:157:26)\n at BidService.processPendingTrades (C:\Users\seonghyeon\coding\boostcamp\membership\project\refactor-web03-CorinEE\packages\server\src\trade\trade-ask-bid.service.ts:46:9)\n at Timeout.processBidTrades (C:\Users\seonghyeon\coding\boostcamp\membership\project\refactor-web03-CorinEE\packages\server\src\trade\trade-bid.service.ts:35:5)"]}
- 불필요한 트랜잭션 commit으로 인해 발생하는 문제 수정
redis db 분리
- 미체결 데이터를 저장하는 redis db를 개발서버와 배포서버를 같이 사용하여 발생하는 문제 수정
사용자가 많을 경우 거래 체결 지연 발생(수정 중)
현재 상황
1차 trade 테스트 진행(100명)
모든 사용자가 ETH 600만원 1개 구매
trade_result_start_origin
- 매수 주문 적용된 시간 측정
- csv column : 사용자번호,매수시간
trade_result_origin
- 매수 주문 넣은 시간에서 거래 체결까지 걸린 시간 측정
- csv column : 사용자번호,걸린시간,매수시간
trade 테스트 시나리오
사용자 : 100명
횟수 : 1번
코인 column : 고정 → 모든 사용자가 같은 코인 거래, 랜덤 → 사용자마다 다른 코인 거래
코인 | 갯수 | 가격 | 테스트 여부(Y/N) | |
---|---|---|---|---|
고정 | 1개(ETH) | 시장가 or 높은가격 | 매수 | Y |
고정 | 2개(ETH, BTC) | 시장가 or 높은가격 | 매수 | |
고정 | 3개 이상 | 시장가 or 높은가격 | 매수 | |
랜덤 | 3개 이상 | 시장가 or 높은가격 | 매수 | |
고정 | 1개(ETH) | 랜덤 가격 | 매수 | |
고정 | 2개(ETH, BTC) | 랜덤 가격 | 매수 | |
고정 | 3개 이상 | 랜덤 가격 | 매수 | |
랜덤 | 3개 이상 | 랜덤 가격 | 매수 | |
고정 | 1개(ETH) | 시장가 or 낮은가격 | 매도 | |
고정 | 2개(ETH, BTC) | 시장가 or 낮은가격 | 매도 | |
고정 | 3개 이상 | 시장가 or 낮은가격 | 매도 | |
랜덤 | 시장가 or 낮은가격 | 매도 | ||
고정 | 1개(ETH) | 랜덤 가격 | 매도 | |
고정 | 2개(ETH, BTC) | 랜덤 가격 | 매도 | |
고정 | 3개 이상 | 랜덤 가격 | 매도 | |
랜덤 | 랜덤 가격 | 매도 |
추가 정보
- 로직 이해도를 높이고 효율적인 테스트를 진행하기 위해 시나리오 문서화 진행 중
- JMeter를 통해 현재 코드의 문제점을 발견했고 개선 진행 중