week5 멘토링 일지 - boostcampwm2023/web04-ALGOCEAN GitHub Wiki
이번주에 우리 팀이 되고 싶은 모습을 상상하며 체크리스트를 추가해봐도 좋습니다.
멘토가 보기에 우리 팀은 어떤지 의견을 구해보세요.
- 사용자가 서비스를 사용할 수 있는 수준으로 주요 기능이 개발되었다.
- GitHub 저장소만 봐도 프로젝트 개요, 기술적 도전, 구현 과정을 누구나 알 수 있다.
- 나와 우리 팀의 기술적인 자랑 거리나 강점이 무엇인지 그 이유와 함께 설명할 수 있다.
- 6주차에 리팩토링 또는 개선할 영역이 무엇인지 인지하고 있다.
멘토링 이후에는 결론과 To Do를 작성하고 실천해보세요.
멘토에게도 공유하여, 의도와 다르게 정리되지는 않았는지 검토를 받아볼수도 있습니다.
멘토가 미리 아젠다와 질문을 보고 올 수 있도록 사전에 준비하여 공유합니다.
- 로드맵
- FE & BE 질문
-
refreshtoken을 쿠키에서 담아 전송하는데, 클라이언트에서 withCredential을 true로, 백엔드에서 Access-Control-Allow-Credentials을 true로 설정해줬는데도
-
코태기가 왔습니다. 멘토님은 코태기 어떻게 극복 하시나요.
-
저희가 다음주에 발표를 해야 하는데, 무엇을 중점으로 발표를 준비해야 할까요. 멘토님들을 포함한 발표를 듣는 사람들이 가장 듣고 싶어 하는 것이 무엇일지 궁금합니다.
-
대현님 탁구 자주 치시나요.
-
대현님 캠핑 좋아하시나요. 항상 배경이 캠핑이시네요.
-
왜 Denny신가요. 애칭이신가요? (임대현의 대를 따서 대니인지 궁금합니다)
-
대현님과 커피챗 하려면 어디로 가야하나요.
-
대현님은 연말 계획이 어떻게 되시나요. 🎄 (등산은 다이어트의 적이다…)
- TPS 개선 (병목 찾기)
가운데에 TPS 가 안 좋은 부분은 DNS 를 거쳐서 요청을 보냈을 때의 결과.
양 옆은 k8s 클러스터에 요청, ncp server ip에 직접 요청
cdn 을 거쳐서인지 도메인으로 접속했을 때만 TPS가 급격히 떨어집니다.
→ 질문 중에 생각이 나서 문제 해결했습니다. cloudflare proxy 해제
- ORM 쿼리 최적화
# create question
BEGIN
# userId를 외래키로 갖고 있는 question 테이블에 row 생성
SELECT `algocean`.`User`.`Id` FROM `algocean`.`User` WHERE (`algocean`.`User`.`Id` = ? AND 1=1)
INSERT INTO `algocean`.`Question` (`Id`,`UserId`,`Title`,`Content`,`Tag`,`ProgrammingLanguage`,`OriginalLink`,`IsAdopted`,`CreatedAt`,`UpdatedAt`,`ViewCount`,`LikeCount`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
SELECT `algocean`.`Question`.`Id`, `algocean`.`Question`.`UserId`, `algocean`.`Question`.`Title`, `algocean`.`Question`.`Content`, `algocean`.`Question`.`Tag`, `algocean`.`Question`.`ProgrammingLanguage`, `algocean`.`Question`.`OriginalLink`, `algocean`.`Question`.`IsAdopted`, `algocean`.`Question`.`CreatedAt`, `algocean`.`Question`.`UpdatedAt`, `algocean`.`Question`.`ViewCount`, `algocean`.`Question`.`LikeCount`, `algocean`.`Question`.`DeletedAt` FROM `algocean`.`Question` WHERE `algocean`.`Question`.`Id` = ? LIMIT ? OFFSET ?
# 해당 user에 point 부여
SELECT `algocean`.`User`.`Id` FROM `algocean`.`User` WHERE (`algocean`.`User`.`Id` = ? AND 1=1)
UPDATE `algocean`.`User` SET `Points` = (`algocean`.`User`.`Points` + ?) WHERE (`algocean`.`User`.`Id` IN (?) AND (`algocean`.`User`.`Id` = ? AND 1=1))
SELECT `algocean`.`User`.`Id`, `algocean`.`User`.`UserId`, `algocean`.`User`.`Password`, `algocean`.`User`.`Nickname`, `algocean`.`User`.`ProfileImage`, `algocean`.`User`.`Points`, `algocean`.`User`.`CreatedAt`, `algocean`.`User`.`UpdatedAt`, `algocean`.`User`.`DeletedAt`, `algocean`.`User`.`GithubId` FROM `algocean`.`User` WHERE `algocean`.`User`.`Id` = ? LIMIT ? OFFSET ?
# point history 생성
SELECT `algocean`.`User`.`Id` FROM `algocean`.`User` WHERE (`algocean`.`User`.`Id` = ? AND 1=1)
INSERT INTO `algocean`.`Point_History` (`Id`,`UserId`,`PointChange`,`Reason`,`ChangeDate`) VALUES (?,?,?,?,?)
SELECT `algocean`.`Point_History`.`Id`, `algocean`.`Point_History`.`UserId`, `algocean`.`Point_History`.`PointChange`, `algocean`.`Point_History`.`Reason`, `algocean`.`Point_History`.`ChangeDate` FROM `algocean`.`Point_History` WHERE `algocean`.`Point_History`.`Id` = ? LIMIT ? OFFSET ?
# 질문 초안 삭제 ( 초안 생성 후 질문을 생성하는 방식을 사용했음 )
SELECT `algocean`.`Question_Temporary`.`Id`, `algocean`.`Question_Temporary`.`UserId`, `algocean`.`Question_Temporary`.`Title`, `algocean`.`Question_Temporary`.`Content`, `algocean`.`Question_Temporary`.`Tag`, `algocean`.`Question_Temporary`.`ProgrammingLanguage`, `algocean`.`Question_Temporary`.`OriginalLink`, `algocean`.`Question_Temporary`.`CreatedAt` FROM `algocean`.`Question_Temporary` WHERE (`algocean`.`Question_Temporary`.`Id` = ? AND 1=1) LIMIT ? OFFSET ?
DELETE FROM `algocean`.`Question_Temporary` WHERE (`algocean`.`Question_Temporary`.`Id` = ? AND 1=1)
COMMIT
-
sse 관련 코드
import { Injectable } from '@nestjs/common'; import { ReplaySubject } from 'rxjs'; import { SendAnswerDto } from './dto/send-answer.dto'; import { PrismaService } from '../prisma.service'; import { Redis, InjectRedis } from '@nestjs-modules/ioredis'; @Injectable() export class SseService { private readonly publishClient: Redis; private readonly subscribeClient: Redis; private readonly userSubjects: Record<number, ReplaySubject<string>>; constructor( private prisma: PrismaService, @InjectRedis() private readonly redisPublisher: Redis, @InjectRedis() private readonly redisSubscriber: Redis, ) { this.publishClient = this.redisPublisher.duplicate(); this.subscribeClient = this.redisSubscriber.duplicate(); this.userSubjects = {}; } 모든 것들을 sub하는 것보다 필요한 정보만 얻을 수 있도록 코드 고쳐보기 sub의 채널을 정해주는 기능을 넣자 async sendNotificationToUser(userId: number, alarmMessage: SendAnswerDto) { const userKey = `user:${userId}`; // Redis를 사용하여 알림을 저장 await this.publishClient.publish(userKey, JSON.stringify(alarmMessage)); } async createSseStreamForUser(userId: number): Promise<ReplaySubject<string>> { if (!this.userSubjects[userId]) { this.userSubjects[userId] = new ReplaySubject<string>(10); // Redis Pub/Sub을 통한 메시지 구독 await this.subscribeClient.subscribe(`user:${userId}`); this.subscribeClient.on('message', (channel, message) => { if (channel === `user:${userId}`) { this.handleRedisMessage(userId, JSON.parse(message)); } }); } // NestJS 서버 재시작 후에 Redis에서 데이터를 가져오기 위해 Prisma 사용 const pendingNotifications = await this.prisma.notification.findMany({ where: { UserId: userId, IsRead: false, }, }); // pendingNotifications에 저장된 알림을 SendAnswerDto 형태로 변환하여 SSE 스트림에 전달 pendingNotifications.forEach((notification) => { const alarmMessage: SendAnswerDto = { questionId: notification.QuestionId, questionTitle: notification.QuestionTitle, answerId: notification.AnswerId, answerCreatedDate: notification.AnswerCreatedAt, }; this.userSubjects[userId].next(JSON.stringify(alarmMessage)); }); await this.prisma.notification.updateMany({ where: { UserId: userId, IsRead: false, }, data: { IsRead: true, }, }); return this.userSubjects[userId]; } // SSE 스트림을 구독할 때 메시지 처리 async handleRedisMessage(userId: number, alarmMessage: SendAnswerDto) { // 여기서는 알림을 처리하고 DB에 저장할지 여부를 결정 if (this.shouldStoreNotificationInDB(userId)) { await this.storeNotificationInDB(userId, alarmMessage); } else { // DB에 저장하지 않을 경우 SSE 스트림에 알림 추가 this.userSubjects[userId].next(JSON.stringify(alarmMessage)); } } private shouldStoreNotificationInDB(userId: number): boolean { return !this.userSubjects[userId]; } async storeNotificationInDB(userId: number, alarmMessage: SendAnswerDto) { await this.prisma.notification.create({ data: { UserId: userId, QuestionId: alarmMessage.questionId, QuestionTitle: alarmMessage.questionTitle, AnswerId: alarmMessage.answerId, AnswerCreatedAt: alarmMessage.answerCreatedDate, IsRead: false, }, }); } // Redis에서 사용자 키 삭제 removeSseStreamForUser(userId: number) { this.subscribeClient.unsubscribe(`user:${userId}`); delete this.userSubjects[userId]; } }
-
user 연결 정보(stream)를 map에 저장할 수 밖에 없습니다.
-
다른 구현 사례를 찾아봐도 db에 연결 정보를 저장하든 다른 방법을 쓰든 결국엔 map과 관련된 자료 구조를 활용합니다.
-
하지만 저번 주 멘토링 때 in-memory 기반으로 연결 정보를 관리하지 말라고 하셨는데 이 말씀이 좀 불가능한 말씀 아닌 가 하는 생각이 들었습니다.
-
여기까지가 제 생각인데 잘못된 부분을 알고 싶습니다.
-
보문님께서 프리랜서 활동을 하시게 된 스토리가 궁금합니다.
-
보문님 취미생활이 어떻게 되시나요?
멘토링 시간에 나눈 이야기가 휘발되지 않도록 기록해보세요.
- refreshToken
- 서버에서 알아서 처리하는 것이 좋음
- db에 uuid, user, refreshToken을 저장하는게 좋을 듯 함
- 쿠키에 쿠워서 줄 때 이름을 refreshToken 말고 다른 것으로 사용해야함
- 일단 jwt형식인 것을 파악하면 rainbow table 공격을 시도할 가능성이 있음
- 쿠키는 credential true설정, preflight off 설정하여 사용 중
- OAuth
- 오픈 API 느낌
- 다른 서비스와 연동할 때 사용함
- 다른 서비스와 연동하지 않을 때에는 사용을 안하는 것이 좋음
- fetch vs axios
- HTML5 표준이 fetch임
- fetch는 upload, download progress를 못 줌
- 이런 단점을 보완하고자 axios 커스터마이징 하여 사용
- axios 사용할지 fetch 사용할지에 대한 helper function을 통해 편하게 사용 중
- 코태기 극복
- 목적성이 있으면 일이 재밌어짐 (돈, 인생에서 더 좋은 평가를 받는 기회)
- 코딩을 안하고 다른 것을 함. 3 ~ 4 일 지나면 코딩하고 싶어짐
- 발표 준비
- 프로젝트 만든 과정 얘기하기
- 미완성인 부분은 어떻게 만들지 얘기하기
- 성능 개선 포인트 or 어려웠던 일 해결 과정 얘기하기
- 탁구의 장점
- 나이먹어도 꾸준히 할 수 있음(60살에도 가능)
- 코치한테 배운 탁구는 군대 탁구와 많이 다름 선수처럼 비슷하게 칠 수 있음 이게 정말 재밌음
- 캠핑
- 불멍때리면 아무 생각 안해도 시간이 엄청 빨리 지나감
- 힘들 때 가면 좋음
- 데니 어원
- 호주를 갔는데 영어 이름을 지어야 했음
- 호주에서는 danny임 → 이건 대니가 아닌 것 같았음
- 대현을 빠르게 발음해보니 데니가 되어서 denny로 지음
- cloud flare
- SSL 프록시 사용이유 → 인증서를 cloud flare꺼 사용하려고
- SSL 사용 안하고 DNS 원리로만 사용할 거면 렛츠 인크립트 등을 통해 인증서를 받아서 세팅을 해줘야함
- cloudflare LB
- TPS 측정
- Jmeter 사용 시 db 사용 요청은 1000이하로 떨어짐
- 1100이 됐을 때는 누수가 시작 됨 → 응답 못하는 API 발생
- 언제 서버를 scale-up할지 기준을 정해야 함
- 보수적으로 500, 600정도에서 서버를 준비해 놓는 게 좋음
- scale-up 기준
- CPU와 memory를 보고 판단
- CPU를 20 or 25% 잡고 시작 후 부하를 주어 해당 수치를 넘겨보기
- CPU 기준 50이하로 설정 추천
- ORM → SQL로 전환 추천
- n+1문제 같은 걸 지나칠 수 있음
- ORM 설정 찾아보기
- prisma 문서에서 알아보기
- SSE
- 특정 채널에만 publish하도록 기능 수정하면 성능 향상될 것임
- 오토스케일링
- 앞단에서 처리
- 취업 준비 조언
- 일단 계속 달려나가야 함
- 쉬고 달리고를 반복하며 코피를 흘려야 함
- 취업이라는 관문을 넘기 전까지 해당 과정을 계속 수행해야 함
- 다만 즐겁게 하는 것이 좋음
- 프리랜서
- 공기업 취직 후 개발에 대한 욕구가 생김
- 20대 초반에 퇴사하고 프리랜서 느낌으로 외주를 진행
- 생각보다 너무 잘되고 있어서 5 ~ 6 년째 하는 중