연애 매칭 웹사이트 AI프로젝트 회고록

허창원·2023년 8월 21일
0
post-thumbnail
post-custom-banner

👩‍❤️‍👨프로젝트 개요

  • 서비스명: KnockKnock
  • 개발 기간: 2023.07.10 ~ 2023.08.11 (5주)
  • 주제: 같은 가치관을 공유하는, 진지한 연애를 원하는 사회인을 대상으로 심층적인 만남의 기회를 제공하는 서비스
  • 목표
    • RESTful API를 설계하고 구현하는 방법을 배웁니다.
    • JWT를 이용한 사용자 인증방식과 데이터 암호화에 대한 지식을 쌓습니다.
    • 클라이언트와 서버 간의 상호작용, 요청-응답 모델 등 기본적인 작동 원리를 이해합니다.
    • CRUD를 구현할 수 있습니다.
    • MySQL 데이터베이스의 ERD 설계와 관리 방법을 배웁니다.
    • AWS 클라우드 서비스의 EC2, RDS 서비스를 이용해 애플리케이션을 구축할 수 있습니다.
    • socket.io를 이용한 채팅 환경을 이해합니다.
  • API 문서: 프로젝트 API 문서
  • 프로젝트 배포: 프로젝트 웹 사이트
  • GitHub 레포지토리: 프로젝트 레포지토리
  • 테스트 계정
      • 아이디: elice5@test.com
      • 비밀번호: 1q2w3e4r!
      • 아이디: elice6@test.com
      • 비밀번호: 1q2w3e4r!

⭐사용 기술스택

Front-End

  • HTML, CSS, JavaScript, React

Back-End

  • JavaScript, Node.js, MySQL

AI

  • Python, Flask, Pandas, NumPy, Matplotlib

⭐팀원

  • Front-End
    정원석, 최우현, 정유진

  • Back-End
    허창원, 정재훈, 이은석

  • AI
    정원석

⭐아키텍처

⭐로그인 설계

👩‍❤️‍👨서비스 기능

⭐뷰티톡톡 (AI 자가 진단)

  • 퍼스널 컬러 : 개인 피부색에 어울리는 퍼스널 컬러 추천
  • beautyGAN : 사진 분석을 통해 어울리는 메이크업을 해줌

⭐오늘의 낙낙 (연애 매칭)

  • 이 달의 카드를 뽑아 나와 같은 카드를 뽑은 사람을 추천
  • 랜덤으로 6명을 추천

⭐히히낙낙 (만남 커뮤니티)

  • 주제별로 만남을 위한 약속 날짜를 정하고 유저들끼리 모이는 커뮤니티
  • 신청자 리스트에서 전체, 남, 여 확인 가능
  • 만남 신청한 사람을 수락, 거절 가능
  • 게시글 참가 후 댓글 작성 가능

⭐채팅

  • 마음에 드는 이성에게 채팅 신청

👩‍❤️‍👨문제점 및 해결 방안

⭐라우터 선언 순서

userRouter.post('/register', RegisterValidationRules, registerValidate, userController.register);

// 로그인
userRouter.post('/login', loginValidationRules, loginValidate, userController.login);

// 로그인 확인
userRouter.use(loginRequired);

// 로그인 검증
userRouter.get('/isLogin', userController.isLogin);

// 유저 정보 불러오기
userRouter.get('/:userId', userParamsValidate, userController.getOtherUserInfo);

// 현재 로그인한 유저 정보 불러오기
userRouter.get('/mypage', userController.getCurrentUserInfo);

// 유저 정보 수정하기(별명, 설명)
userRouter.put('/mypage', userController.update);

// 유저 정보 삭제하기
userRouter.delete('/mypage', userController.delete);

// 유저 비밀번호 확인, 변경
userRouter.put('/mypage/password', setPasswordValidationRules, setPasswordValidate, userController.updatePassword);

// 현재 로그인한 유저가 작성한 게시글 모두 불러오기
userRouter.get('/mypage/posts', userController.getCurrentUserPosts);

// 현재 로그인한 유저의 참여한 게시글 모두 불러오기
userRouter.get('/mypage/participants', userController.getCurrentUserParticipants);

// 오늘의 낙낙(네트워크)페이지 - 랜덤으로 6명 유저 정보 불러오기
userRouter.get('/network', userController.getRandomUsersInfo);

유저 라우터를 작성할 때, /mypage, /network라우터가 모두 유저 정보 불러오기/:userId로 흘러들어갔습니다.
이 해결방안으로는 아래와 같이 고정적인 주소에 대한 라우터를 먼저 작성하고, 그 후 params와 같은 가변적인 주소에 대한 라우터를 후반부에 작성합니다.

userRouter.post('/register', RegisterValidationRules, registerValidate, userController.register);

// 로그인
userRouter.post('/login', loginValidationRules, loginValidate, userController.login);

// 로그인 확인
userRouter.use(loginRequired);

// 로그인 검증
userRouter.get('/isLogin', userController.isLogin);

// 현재 로그인한 유저 정보 불러오기
userRouter.get('/mypage', userController.getCurrentUserInfo);

// 유저 정보 수정하기(별명, 설명)
userRouter.put('/mypage', userController.update);

// 유저 정보 삭제하기
userRouter.delete('/mypage', userController.delete);

// 유저 비밀번호 확인, 변경
userRouter.put('/mypage/password', setPasswordValidationRules, setPasswordValidate, userController.updatePassword);

// 현재 로그인한 유저가 작성한 게시글 모두 불러오기
userRouter.get('/mypage/posts', userController.getCurrentUserPosts);

// 현재 로그인한 유저의 참여한 게시글 모두 불러오기
userRouter.get('/mypage/participants', userController.getCurrentUserParticipants);

// 오늘의 낙낙(네트워크)페이지 - 랜덤으로 6명 유저 정보 불러오기
userRouter.get('/network', userController.getRandomUsersInfo);

// 유저 정보 불러오기
userRouter.get('/:userId', userParamsValidate, userController.getOtherUserInfo);

⭐S3

import 'dotenv/config';
import multer from 'multer';
import multerS3 from 'multer-s3';
import AWS from 'aws-sdk';

const s3 = new AWS.S3({
    region: process.env.AWS_BUCKET_REGION,
    accessKeyId: process.env.AWS_ACCESS_KEY,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});

const upload = multer({
    storage: multerS3({
        s3: s3,
        bucket: process.env.AWS_BUCKET_NAME,
        acl: 'public-read',
        metadata: function (req, file, cb) {
            cb(null, { fieldName: file.fieldname });
        },
        key: function (req, file, cb) {
            const date = new Date().toISOString().replace(/:/g, '-');
            const fileExtension = file.originalname.split('.').pop();
            const filename = `image-${date}.${fileExtension}`;
            cb(null, filename);
        },
    }),
});

export { upload };

S3는 깊이 공부할 시간이 없어 public으로 사용했습니다. 실무에서는 절대로 public으로 사용해서는 안된다는 것을 알게되었고 추후 고도화 할때 presigned를 적용하여 보안을 강화할 것입니다.

⭐트랜잭션

이번 프로젝트에서 모임 게시글에 대해 간단히 설명하면, 신청자가 게시글에 참여 신청하고 게시자가 수락했을 때, 신청자는 '대기중'에서 '수락됨'으로 변경되고 게시글은 참가자가 +1명으로 상태가 변합니다.

이때, 참여신청 과정과 수락 과정 중 하나라도 실행에 실패하면 신청자는 대기중인데 참가자가 +1명이 되거나 반대 상황이 발생하여 데이터베이스의 일관성이 깨지게 됩니다.

이를 해결하기 위해 트랜잭션을 도입했습니다.

그러나 트랜잭션을 도입했음에도 작동하지 않았습니다.

Before

// participantService.js
const transaction = await db.sequelize.transaction({ autocommit: false }); // 트랜잭션 생성
        try {
          
            ...

            await ParticipantModel.update({ participantId, updateField: 'status', newValue: 'accepted' });
            await PostModel.update({ postId, fieldToUpdate, newValue });
            if (isCompleted) {
                await PostModel.update({ postId, fieldToUpdate: 'isCompleted', newValue: true });
            }
            await transaction.commit();

            return {
              
                ...
              
            };
        } catch (error) {
            await transaction.rollback();
            
          ...
          
        }
// ParticipantModel.js
update: async ({ participantId, updateField, newValue }) => {
        await db.Participant.update(
            { [updateField]: newValue },
            {
                where: { participantId },
            },
        );
    },
// PostModel.js
update: async ({ postId, fieldToUpdate, newValue }) => {
        const updatePost = await db.Post.update(
            { [fieldToUpdate]: newValue },
            {
                where: { postId },
            },
        );
        return updatePost;
    },

트랜잭션이 작동하지 않았던 이유는 DB와 상호작용하는 ParticipantModel.js, PostModel.js에서 직접 transaction을 설정해주지 않았지 때문입니다. 어떤 하나의 작업이 하나의 transaction임을 선언해주어야 한다는 것을 깨닫게 되었습니다.

After

// participantService.js
const transaction = await db.sequelize.transaction({ autocommit: false }); // 트랜잭션 생성
        try {
          
            ...

            await ParticipantModel.update({ transaction, participantId, updateField: 'status', newValue: 'accepted' });
            await PostModel.update({ transaction, postId, fieldToUpdate, newValue });
            if (isCompleted) {
                await PostModel.update({ transaction, postId, fieldToUpdate: 'isCompleted', newValue: true });
            }
            await transaction.commit();

            return {
              
                ...
              
            };
        } catch (error) {
            await transaction.rollback();
            
          ...
          
        }
// ParticipantModel.js
update: async ({ transaction, participantId, updateField, newValue }) => {
        await db.Participant.update(
            { [updateField]: newValue },
            {
                where: { participantId },
                transaction,
            },
        );
    },
// PostModel.js
update: async ({ transaction, postId, fieldToUpdate, newValue }) => {
        const updatePost = await db.Post.update(
            { [fieldToUpdate]: newValue },
            {
                where: { postId },
                transaction,
            },
        );
        return updatePost;
    },

⭐데이터 모델링

사용자들의 취미, 이상형, 성격 등에 대한 태그 데이터를 데이터베이스에 저장하여 연인 매칭 기능을 구현하려는 계획을 세웠습니다. 처음에는 태그 데이터를 배열로 저장하는 방식을 고려했으나, MySQL은 배열 저장을 지원하지 않아 이 방법은 실행할 수 없었습니다. 더불어 배열 형태로 데이터를 저장하게 되면 사용자 간의 관계 파악이 어려워져 태그 데이터 조회에도 문제가 발생하는 상황이었습니다.

그래서 문제를 해결하기 위해, 매핑 테이블을 도입했습니다. 이 매핑 테이블은 사용자 ID, 태그 카테고리 ID, 그리고 태그 ID를 저장하고 있습니다. 회원 테이블, 회원-태그(매핑 테이블), 그리고 태그 테이블 - 이 세 개의 테이블을 활용하여 각 엔티티 간의 관계를 명확하게 정립했습니다. 이런 방식으로 설계된 ERD는 사용자가 선택한 태그의 수가 다르더라도 데이터 조회가 용이하며, 추가적인 태그 카테고리나 태그 삽입 등 시스템 확장에도 유연하게 대응할 수 있는 구조로 만들었습니다.

⭐커서 페이지네이션

댓글 페이지네이션은 어떻게할지 고민하였고 페이지네이션에는 두 종류가 있다는 것을 알게 되었습니다. 오프셋 페이지네이션과 커서 페이지네이션의 장단점을 비교한 후, 이 프로젝트에서는 댓글을 무한 스크롤로 구현을 할 것이기 때문에 커서 페이지네이션이 더 적합하다고 결론을 내렸습니다.

데이터를 약 260만개를 넣고 두 페이지네이션 방법의 조회 속도를 비교해 보았습니다.

커서 페이지네이션

오프셋 페이지네이션
커서 페이지네이션은 0.478초, 오프셋 페이지네이션은 10.34초가 걸렸습니다. 약 20배만큼 커서 페이지네이션이 압도적으로 빠르다는 것을 확인할 수 있었습니다.

👩‍❤️‍👨프로젝트 성과

엘리스에서 우수상을 수상 받았습니다. 기능 수는 적지만 처음 기획했던 기능을 모두 구현했고 팀원들과 마지막까지 버그를 수정하여 얻은 결과라고 생각합니다.

👩‍❤️‍👨배운점

커뮤니케이션에서 중요한 요소 중 하나는 기록이라고 생각합니다. 우리는 노션에 프로젝트 페이지를 생성하여 회의록을 작성하고, 팀원들과 소통하며 개발 방향성을 정할 수 있었습니다. 이를 통해 개발 진행 상황을 공유하고, 프론트엔드의 진행 상황을 파악하여 백엔드에서 도움이 필요한 부분에 대해 지원할 수 있었습니다. 이번 프로젝트를 통해 개발 흐름을 파악하고, 이전 프로젝트에서 부족한 부분을 보완하여 적용할 수 있는 기회가 되었습니다. 덕분에 MVC 패턴으로 파일 구조화하는 방법, ERD 설계의 확장성 고려, REST API 작성 등 백엔드 개발자로서 고려해야 할 기본 사항들에 대해 깊이 생각할 수 있는 시간이 되었습니다.

👩‍❤️‍👨추후 개선 방향

추후에는 Javascript를 Typescript로 변경하여 프로젝트를 고도화해보고 싶고 Jenkins, Docker 등을 추가로 사용해보려고 합니다. 이번에 JWT 토큰으로 로그인을 설계했지만 만료시간 등을 설정하지 않아 부족한 보안을 보완할 계획입니다.

post-custom-banner

0개의 댓글