week5 멘토링 일지 - boostcampwm2023/web04-ALGOCEAN GitHub Wiki

week5 멘토링 일지

✅ 체크리스트

이번주에 우리 팀이 되고 싶은 모습을 상상하며 체크리스트를 추가해봐도 좋습니다.

멘토가 보기에 우리 팀은 어떤지 의견을 구해보세요.

  • 사용자가 서비스를 사용할 수 있는 수준으로 주요 기능이 개발되었다.
  • GitHub 저장소만 봐도 프로젝트 개요, 기술적 도전, 구현 과정을 누구나 알 수 있다.
  • 나와 우리 팀의 기술적인 자랑 거리나 강점이 무엇인지 그 이유와 함께 설명할 수 있다.
  • 6주차에 리팩토링 또는 개선할 영역이 무엇인지 인지하고 있다.

✔️ 결론 및 To Do

멘토링 이후에는 결론과 To Do를 작성하고 실천해보세요.

멘토에게도 공유하여, 의도와 다르게 정리되지는 않았는지 검토를 받아볼수도 있습니다.

✔️ 아젠다 및 질문

멘토가 미리 아젠다와 질문을 보고 올 수 있도록 사전에 준비하여 공유합니다.

진행 순서

  • 로드맵
  • FE & BE 질문

FE 질문

  • refreshtoken을 쿠키에서 담아 전송하는데, 클라이언트에서 withCredential을 true로, 백엔드에서 Access-Control-Allow-Credentials을 true로 설정해줬는데도

  • 코태기가 왔습니다. 멘토님은 코태기 어떻게 극복 하시나요.

  • 저희가 다음주에 발표를 해야 하는데, 무엇을 중점으로 발표를 준비해야 할까요. 멘토님들을 포함한 발표를 듣는 사람들이 가장 듣고 싶어 하는 것이 무엇일지 궁금합니다.

  • 대현님 탁구 자주 치시나요.

  • 대현님 캠핑 좋아하시나요. 항상 배경이 캠핑이시네요.

  • 왜 Denny신가요. 애칭이신가요? (임대현의 대를 따서 대니인지 궁금합니다)

  • 대현님과 커피챗 하려면 어디로 가야하나요.

  • 대현님은 연말 계획이 어떻게 되시나요. 🎄 (등산은 다이어트의 적이다…)

BE 질문

  • TPS 개선 (병목 찾기)

image 가운데에 TPS 가 안 좋은 부분은 DNS 를 거쳐서 요청을 보냈을 때의 결과.

양 옆은 k8s 클러스터에 요청, ncp server ip에 직접 요청

cdn 을 거쳐서인지 도메인으로 접속했을 때만 TPS가 급격히 떨어집니다.

→ 질문 중에 생각이 나서 문제 해결했습니다. cloudflare proxy 해제

image image

  • 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 년째 하는 중
⚠️ **GitHub.com Fallback** ⚠️