-
Notifications
You must be signed in to change notification settings - Fork 1
[개별 멘토링] 2024.11.24
-
랭킹을 위한 Redis 사용
- 랭킹 조회 혹은 최근 검색 기록 조회 시 매번 ORDER BY 연산이 필요하며 이로 인한 자원 낭비가 발생한다.
- 동시 접속자 증가에 따른 DB 부하가 증가한다.
- 최근 검색어의 경우 빈번한 I/O 가 필요하다.
- Sorted Set 을 이용해 효율적으로 랭킹 관리
- 인메모리 데이터 처리로 빠른 응답 속도
- 사용자 증가에 따른 빈번한 DB 이용에 따른 부하를 줄일 수 있음
기존에는 랭킹을 검색할 때마다 Asset table을 Order By를 이용해 Sort 하는 상황이 반복되었고 이로 인해 생기는 부담을 줄이고자 Rankings 테이블을 구성하였다. 하지만 Asset 테이블과 겹치는 부분이 많고 랭킹 조회시 user_id 를 이용해 user Table과 Join하여 유저의 닉네임을 가져와야 하기 때문에 이로 인한 부하도 생길 것으로 예측 하였다.
이에 따라 Redis를 이용해 Ranking 데이터를 저장하고 제공하는 로직을 구성해보았다.
-
redis.domain-service.ts
import { Injectable, Inject } from '@nestjs/common'; import Redis from 'ioredis'; @Injectable() export class RedisDomainService { constructor( @Inject('REDIS_CLIENT') private readonly redis: Redis, ) {} async get(key: string): Promise<string | null> { return this.redis.get(key); } async set(key: string, value: string, expires?: number): Promise<'OK'> { if (expires) { return this.redis.set(key, value, 'EX', expires); } return this.redis.set(key, value); } async del(key: string): Promise<number> { return this.redis.del(key); } async zadd(key: string, score: number, member: string): Promise<number> { return this.redis.zadd(key, score, member); } async zcard(key: string): Promise<number> { return this.redis.zcard(key); } async zrange(key: string, start: number, stop: number): Promise<string[]> { return this.redis.zrange(key, start, stop); } async zremrangebyrank( key: string, start: number, stop: number, ): Promise<number> { return this.redis.zremrangebyrank(key, start, stop); } async zrevrange(key: string, start: number, stop: number): Promise<string[]> { return this.redis.zrevrange(key, start, stop); } async zrem(key: string, member: string): Promise<number> { return this.redis.zrem(key, member); } async expire(key: string, seconds: number): Promise<number> { return this.redis.expire(key, seconds); } }
-
ranking.service.ts
async getRanking(sortBy: SortType = SortType.PROFIT_RATE) { const date = new Date().toISOString().slice(0, 10); const key = `ranking:${date}:${sortBy}`; if (await this.redisDomainService.exists(key)) { return { topRank: await this.redisDomainService.zrevrange(key, 0, 9), userRank: null, }; } const ranking = await this.calculateRanking(sortBy); await Promise.all( ranking.map((rank: Ranking) => this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), rank.nickname, ), ), ); return { topRank: await this.redisDomainService.zrevrange(key, 0, 9), userRank: null, }; }
redis를 여러 service에서 참조해야하기 때문에 service layer 사이의 참조는 좋지 않다고 생각하여 domain-service 레이어를 하나 만들어 다른 서비스가 참조 할 수 있도록 하였다.
-
Nginx 를 이용한 프록시 서버 구축 및 SSL 설정
작성한 config 파일
events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # WebSocket 연결을 위한 map 설정 map $http_upgrade $connection_upgrade { default upgrade; '' close; } # HTTP 서버 블록 server { listen 80; server_name juga.kro.kr; location /.well-known/acme-challenge/ { root /var/www/certbot; } # HTTP를 HTTPS로 리다이렉트 location / { return 301 https://$host$request_uri; } } # HTTPS 서버 블록 server { listen 443 ssl; # SSL 인증서 사용하여 443 포트에서 리스닝 server_name juga.kro.kr; ssl_certificate /etc/letsencrypt/live/juga.kro.kr/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/juga.kro.kr/privkey.pem; # Socket.IO 설정 location /socket { proxy_pass http://juga-docker-be:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_buffers 8 32k; proxy_buffer_size 64k; proxy_connect_timeout 75s; proxy_send_timeout 75s; proxy_read_timeout 75s; } # Frontend proxy location / { proxy_pass http://juga-docker-fe:80; # 프록시를 위한 HTTP 프로토콜 버전을 정의 proxy_http_version 1.1; proxy_set_header Host $host; # 방문자의 실제 IP 주소 전달 proxy_set_header X-Real-IP $remote_addr; # 프록시 정보 전달 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 프로토콜 정보 전달 proxy_set_header X-Forwarded-Proto $scheme; #Web socket 업그레이드 proxy_set_header Upgrade $http_upgrade; #Web socket 연결 유지 proxy_set_header Connection $connection_upgrade; # try_files 대신 error_page 사용 proxy_intercept_errors on; error_page 404 = /index.html; } # Backend proxy location /api { proxy_pass http://juga-docker-be:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } }
동료와 공유하거나 의견을 묻고 싶은 부분을 정리해보세요.
피어세션에서 나눈 이야기 중 나의 문제 해결 경험과 관련한 피드백과 새롭게 깨달은 점이 있다면 기록으로 남겨주세요.
-
배포 주소
질문이나 피드백 있으시면 이야기 해주세요!
-
테스트 코드 작성에 대한 질문
-
리팩토링 주간을 맞아 본격적으로 리팩토링을 진행하기 위해서 테스트 코드를 작성하는 것도 좋은 선택이라고 생각합니다. 이에 기능 마무리가 되면 조금씩 테스트 코드를 작성해보는 것도 좋을 것 같아 테스트 코드에 대한 개념을 잘 잡아가고 싶습니다.
-
Unit 테스트와 통합 테스트의 차이점은 개별 단위의 동작 검증과 여러 단위별의 상호 작용이 잘 되는지 확인하는데 있다고 알고 있습니다. 그렇기 때문에 저번 테스트 코드 작성에서 Controller, service, model 별로 각각의 영역에서 잘 작동하는지 다른 레이어는 목킹을 하거나 해당 레이어가 호출되어야 한다는 코드를 넣는 등 의 작업을 통해 테스트 해보려고 했는데 통합 테스트 같다는 피드백을 받은 적이 있어 어떤 식으로 수정해야할지 여쭤보고 싶습니다.
지적 받은 코드
// controller 테스트 코드 describe('handleRegister 메소드', () => { it('유효한 사용자 데이터로 회원가입을 처리해야 한다', async () => { const mockRequest: HTTPRequest = { body: JSON.stringify({ userId: '[email protected]', email: '[email protected]', password: 'password123', username: 'testuser', }), method: 'POST', url: '/api/create', headers: { host: 'localhost:3000', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0', accept: '*/*', 'accept-language': 'ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3', 'accept-encoding': 'gzip, deflate, br, zstd', referer: 'http://localhost:3000/user/form.html', 'content-type': 'application/json', 'content-length': '111', origin: 'http://localhost:3000', connection: 'keep-alive', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin', priority: 'u=0', }, }; const mockSocket = new net.Socket(); const response = new HTTPResponse(mockSocket); await UserController.handleRegister(mockRequest, response); expect(UserService.registerUser).toHaveBeenCalledWith( '[email protected]', 'password123', 'testuser' ); }); }); // service 테스트 코드 describe('UserService 테스트 코드', () => { let mockRedis: jest.Mocked<Redis>; beforeEach(() => { mockRedis = new Redis() as jest.Mocked<Redis>; jest.clearAllMocks(); (UserModel.findOne as jest.Mock).mockResolvedValue(null); (UserModel.create as jest.Mock).mockResolvedValue({ id: 1 }); (bcrypt.hash as jest.Mock).mockResolvedValue('hashedPassword'); }); describe('registerUser 메소드 테스트', () => { it('새로운 유저가 회원가입에 성공해야 한다.', async () => { (UserModel.create as jest.Mock).mockResolvedValue({ id: 1 }); await UserService.registerUser('[email protected]', 'password', 'testuser'); expect(UserModel.findOne).toHaveBeenCalledWith({ where: { email: '[email protected]' }, }); expect(bcrypt.hash).toHaveBeenCalledWith('password', 12); expect(UserModel.create).toHaveBeenCalledWith({ email: '[email protected]', password: 'hashedPassword', username: 'testuser', }); }); }); }); // 모델 테스트 코드 test('findOne 테스트', async () => { const result = await UserModel.findOne({ where: { email: testUserInfo.email }, }); expect(result).not.toBeNull(); expect(result).toEqual(testUser); });
-
Controller → service → domainService → repository
- 이중에 service (랭킹, 등등) 를 테스트 하는 방법으로 하는게 좋을 것 같다.
- controller는 그냥 service를 갖다 쓰기만 하면 될거 같고 repository는 데이터를 받아서 그냥 저장만 하면 되는거라 테스트 할 필요가 없을 것 같다.
-
-
5주차 일정에 대한 조언
1주차에 계획했던 필수 기능에 대해 거의 모든 구현을 끝냈고 , 이제 제가 생각하는 들어 가야 할 기능은 친구 기능과 대결 기능일 것이라고 생각합니다. 기획서에도 친구와 같이 성장 하는 재미를 강조하는 만큼 해당 기능이 들어가면 좋을 것 필요할 것 같긴 하지만 예상외로 품이 많이 들 것 같아 망설여집니다.
5주차 발표 내용을 보면 1~5주차의 과정과 6주차 계획을 발표해야하는데 5주차엔 이전 코드를 보면서 수정 및 보완하는 작업을 가지는게 좋을까요? 아니면 해당 기능을 구현하는게 맞을지 의견을 여쭤보고 싶습니다.
- 대결기능까진 투머치고 하게 되면 친구 기능, 프로필 조회
- 실시간 뉴스 , 주식 즐겨찾기 같은 경우는 사소한 기능이기 때문에 기존 주식을 위한 기능의 완성도를 높이고 싶으면 구현해야할 거 같고, 친구 추가는 다른 결의 기능이다. 서비스의 특성을 주고 싶으면 아래, 주식에 초점을 두고 싶으면 위 기능을 하면 좋을 것 같다.
- [FE] 프론트엔드 기술스택
- [FE] 라이브러리 없이 차트 구현 이유
- [FE] Canvas API 사용방법
- [FE] 네비게이션 바 애니메이션 구현
- [FE] Socket.io 사용방법
- [FE] Tanstack Router에 대하여...
- [FE] Intl(Internationalization) API
- [FE] React Suspense 적용
- [FE] 한글 입력 방식의 유연성을 높인 검색 시스템 구현하기
- [BE] 백엔드 기술 스택
- [BE] SSE vs Socket.io
- [BE] Redis를 도입하게 된 계기
- [BE] ACG Rule을 활용한 Secure CI CD 파이프라인 구현
- [BE] Nginx 로드밸런싱을 통해 한국 투자 API 소켓 제한 극복
- [BE] 주가 지수 기능 개발 과정
- [BE] 매수 및 매도 기능 개발 과정
- [BE] 실시간 자산 조회 기능 개발 과정
- [BE] 단위 테스트
- [BE] redis를 이용한 한국투자 Open API 세션 관리
- [BE] 데이터베이스 인덱싱
- [FE] React에서의 DOM 요소 접근 (useRef vs getElementById)
- [FE] Outlet을 활용한 공통 레이아웃 관리
- [FE] react hooks가 특정 조건에서 실행되면 안되는 이유 & useQuery에 query function 매개변수가 undefined일 수도 있을 때 어떻게 해결할까
- [FE] cross‐domain 로컬 환경에서 cookie로 인증 처리하기 with vite proxy
- [FE] 크롬&사파리 Composition 차이
- [FE] useEffect 의존성 배열
- [BE] Naver Cloud Platform HTTPS 무응답 현상
- [BE] 한국투자 Open API에서 access token을 발급받지 못하는 문제
- [BE] 한국투자 Open API와 웹소켓 연결이 되지 않던 문제
- [BE] 한국투자 Open API 웹소켓 연결이 중단되는 문제
- [BE] 같은 주식 주문이 동시에 여러 번 체결되는 문제
- [BE] 한국투자 Open API Websocket 세션을 두 개에서 한 개로 변경하기
- [BE] Nginx 로드 밸런싱 중 Socket bad Request 발생하는 현상
- [BE] 매수/매도 체결 로직에 의해 redis pub/sub이 정상적으로 동작하지 않는 문제