Code Monkey home page Code Monkey logo

hobby-chain's Introduction

💬 취미 공유 SNS 서비스 'hobby-chain' 개발 프로젝트

🎯 프로젝트 목표

  1. 객체 지향 원리를 적용하여 CleanCode를 목표로 유지보수가 용이한 코드 설계

    • SOLID 원리의 이해를 바탕으로 최대한 5가지의 원칙을 준수하기 위해 노력하였습니다.
    • 추상체를 제공하여 구현체가 변경되어도 다른 구현체에는 무리가 없도록 하였습니다.
  2. 단순한 기능 구현이 아닌, 대용량 트래픽을 고려하여 scale-out을 고려한 설계

    • 동시 접속자 수가 많다고 가정하여, 많은 유저를 감당할 수 있는 분산 시스템 아키텍처 구축을 위해 노력하였습니다.
    • HTTP 특성의 이해를 기반으로 세션 및 이미지 스토리지를 사용하여 무상태를 보존하는 등 확장에 용이하도록 노력하였습니다.
  3. 대용량 트래픽 처리를 고려한 설계 및 구현

    • 세션 스토리지에 Redis를 사용하여 디스크 기반 데이터베이스보다 속도를 향상시켜 더 나은 성능을 가질 수 있도록 하였습니다.
    • 데이터베이스에 몰리는 트래픽을 분산시키기 위해 MySQL의 Replication을 통해 Master와 Slave 구조로 시스템을 구축하였습니다.
    • 옵티마이저의 쿼리 실행 계획을 예측해보고 인덱스 설계를 하여 쿼리를 실행하여 분석해보고 조회에 빠른 속도를 제공할 수 있도록 쿼리튜닝을 진행하였습니다.
    • 알림 서비스는 부가적인 기능이라 판단하고 비동기로 작동하는 메세지 큐를 통해 알림 전송 관련 트래픽을 분산 시켰습니다.
    • 속도 향상을 위해 이미지 업로드 시 CompletableFuture를 사용하여 비동기로 구현하였습니다.

⌨️ 사용 기술 및 개발 환경

Language : Java

Framework : Springboot

Database / ORM : MySQL, MyBatis

Session Storage : Redis

IDE : IntelliJ

Development tools: Gradle, Github, Slack

✏️ Architecture

hobby-chain-architecture drawio (1)

🗂️ ERD

인덱스에 대한 자세한 사항은 여기를 확인해 주세요 👉 #20

hobby-chain-db

🔎 주요 기능

  1. 회원가입 / 탈퇴
  2. 로그인 / 로그아웃
  3. 회원정보 수정
  4. 게시글 작성 / 수정 / 삭제 / 조회
  5. 피드 조회
  6. 좋아요 기능
  7. 댓글 작성 / 수정 / 삭제 / 조회
  8. 팔로우 / 언팔로우

hobby-chain's People

Contributors

tjgus027 avatar f-lab-bot avatar

Stargazers

jongheon avatar  avatar JongHyeon Choi avatar

Watchers

 avatar

hobby-chain's Issues

프로젝트 생성

Description

  • 프로젝트 생성 및 초기 셋팅 (gradle 사용)
  • Git Branch 전략 선택 -> Git-flow
  • DB 선택 -> MySQL
  • ORM 선택 -> Mybatis
  • 초기 의존성 라이브러리 설정

[설계] 인덱스

인덱스 설계 수정본

✅ Used DataBase

MySQL

✅ ERD

hobby-chain-erd

✅ Indexes

member

Clustered Index : user_id
Non Clustered Index: email

post
Clustered Index : post_id

post_reply
Clustered Index : reply_id

post_image
Clustered Index : image_id

post_like
Clustered Index : composite_idx (post_id, user_id)
Non Clustered Index: post_id, user_id

member_follow
Clustered Index : composite_idx (follower, followee)
Non Clustered Index: follower, followee

📌 INDEX

✔️ 인덱스 설계 기준

  1. 데이터를 조작하는 작업보다 검색 작업의 수행 비율이 더 높은지?
  2. 검색 시 조건절에 사용하는지?
  3. 데이터 양은 많은지?

✔️ 설계한 인덱스 확인 요소

  1. 쿼리 실행 계획을 분석했을때 옵티마이저가 설계한 인덱스를 이용하는지?
  2. 복합 인덱스 설계 시, 자주 조회하는 컬럼을 앞에 두었는지?

🤔 인덱스를 기준없이 낭비하면 안 되는 이유?

  1. 조회를 제외한 DML 성능 저하

    쓰기 및 수정, 삭제 작업이 이루어질 때 인덱스에서도 작업이 이루어져야 하고, 정렬을 위해 입력 블록을 찾는 탐색 시간도 있으며, 입력 블록에 여유 공간이 없을 시 인덱스 분할이 일어날 수 있어 시간이 지체될 수 있다.

  2. 디스크 공간 낭비

    인덱스를 저장해야 하는 공간도 필요해서 데이터베이스의 사이즈가 증가하고, 인덱스가 없을때보다 10~20% 정도의 디스크 공간을 더 사용한다.

💡Issue - 성능 개선

배경 → 문제 → 해결 → 결과의 흐름으로 작성했습니다

배경

follow 테이블에서 사용하는 조회 쿼리

  1. 팔로우하려는 유저가 이미 팔로우하고 있는지 확인을 위한 쿼리

    SELECT follower, followee FROM memeber_follow WHERE follower = ${follower} AND followee = ${followee}
  2. 유저 아이디로 팔로우 수 조회를 위한 쿼리

    SELECT COUNT(followee) FROM member_follow WHERE follower = ${follower}
  3. 유저 아이디로 팔로잉 수 조회를 위한 쿼리

    SELECT COUNT(follower) FROM member_follow WHERE followee = ${followee}

사용 인덱스

  • 1번을 많은 메소드에서 사용하고, 유저가 자주 사용하게 될 것이라 판단하여 속도 및 성능을 향상 시키기 위해 follower, followee의 순서로 복합 인덱스를 설계

✔️ follow_composite_idx (follower, followee 복합 인덱스)

❓왜 follower_id를 선행 인덱스로 선택했는지?

계획 중인 프로젝트와 비슷한 인스타를 보고 고민해봤을때, 팔로워 아이디 ( = 로그인한 사용자)를 기준으로 검색이 더 많을 것 같아서 따로 follower_id 컬럼의 인덱스를 두지 않아도 인덱스 스캔을 할 것 같아, 위와 같은 순서로 설계 했습니다

인덱스 예시

실행 쿼리: SELECT follower, followee FROM member_follow WHERE follower = 2 AND followee = 8;

문제

자주 사용하는 쿼리 중 하나가 인덱스 스캔이 아닌 테이블 풀 스캔을 통해 실행하기 때문에, 조회 시 속도가 저하되어 성능이 좋지 않다는 문제를 발견

(쿼리 실행 계획 첨부 + 어떤 쿼리를 통해서 검색되어 가는지)

✅ 좋은 성능을 가진 쿼리

✔️ 1번 & 2번
1번 쿼리 - 쿼리 실행 계획 결과를 분석했을 때, 설계한 인덱스를 조건절에 사용했기 때문에 예상한 인덱스 (follow_composite_idx)를 사용하여 Index Range Scan을 통해 일치하는 값을 탐색

2번 쿼리 - 복합 인덱스의 선행 컬럼을 조건절에 사용하도록 설정했기에, 조건을 통해 검색했을 때 Index Scan을 통해 조회

❌ 성능이 좋지 않은 쿼리

✔️ 3번

3번 쿼리 - 조건절에 사용하는 컬럼은 복합 인덱스의 후행 컬럼으로 설정했으나, 데이터베이스에 follower, followee 컬럼만 있었기에, Table Full Scan을 실행하여 속도 ⬇️

index drawio (1)

해결

✔️ followee 컬럼을 인덱스로 추가 설정

결과

3번 쿼리도 설계한 인덱스가 조건절에 사용되기 때문에, 조회 시에 인덱스를 사용하여 성능 향상 (원래는 테이블 풀스캔)

index drawio (2)

💡Issue 2 - 클러스터링 인덱스 VS 세컨더리 인덱스

배경

회원 수정 및 탈퇴, 게시물 조회 등 회원이 존재하는지 확인할 때 사용하는 쿼리

SELECT EXISTS(SELECT user_id FROM member WHERE email || userId = #{email} || #{userId});

email 컬럼은 세컨더리 인덱스 / memberId 컬럼은 클러스터링 인덱스

문제

🤔 세션에 email VS memberId 중 어떤 것을 저장하여 조회할지? 고민

email 컬럼은 세컨더리 인덱스이고, memberId는 클러스터링 인덱스라서 둘을 각각 사용하여 조회했을때

세컨더리 인덱스는 데이터 값에 PK를 저장하기 때문에, 클러스터링 인덱스에 비해 낮은 성능

해결

✔️ 클러스터링 인덱스인 memberId를 세션에 저장하고, 세션 값으로 조회

애플리케이션에서 사용하는 쿼리

SELECT EXISTS(SELECT user_id FROM member WHERE userId = #{userId};

결과

→ 클러스터링 인덱스를 사용함으로써 속도 및 성능 향상

💡Issue 3 - UK 설정

배경

게시물을 좋아요할 때 사용하는 쿼리

INSERT INTO post_like(post_id, user_id) VALUES(#{post_id}, #{user_id});

문제

트랜잭션의 격리 수준을 Repeatable Read로 설정하긴 했으나, 이 격리 수준도 다른 트랜잭션에 의해 새로운 데이터가 생기면 인지하지 못하여 데이터 부정합 문제가 발생할 수 있기 때문에 같은 데이터가 중복 저장 가능

해결

✔️ 조회를 위해 복합 인덱스로 설계했던 컬럼은 Unique Key로 설정하여 같은 값이 데이터베이스에 저장 불가능하도록 변경

결과

트랜잭션이 충돌나서 데이터가 중복 저장되지 않고, 원하던 하나의 튜플만 저장

회원가입

Description

  • 요청 데이터
    • 필수: 이메일(아이디), 비밀번호, 이름, 닉네임, 성별, 전화번호, 나이
  • 회원가입 시 이메일 중복 체크
  • 입력값 유효성 검사
    • 이메일, 비밀번호, 전화번호 정규식을 통해 검사
  • 비밀번호 암호화

[설계] Redis를 사용한 글로벌 세션

📌 세션 vs JWT

🤔 둘 중 어떤 것을 선택해야 하지?

세션

서버 측에서 상태를 유지하는 형태
클라이언트는 세션 ID만 가지고 서버의 세션과 연결

장점: 쿠키에 아무런 의미가 없는 세션ID가 저장이 되므로 탈취되더라도 해석 불가
단점: 서버의 자원을 사용하기 때문에 사용자가 다수일 경우 부하가 높아짐

JWT(Json Web Token)

클라이언트 측에서 상태를 유지하는 형태
토큰에는 회원 정보, 서명, 유효기간 등의 정보를 포함
서버는 토큰의 정보를 보고 연결

장점: 토큰에 정보를 포함하고 있기에 세션에 비해 부하가 적음
단점: 토큰에 넣는 데이터가 많아질 수록 토큰이 길어져 api호출 시마다 토큰 데이터를 서버에 전달할때 그만큼 네트워크 대역폭 낭비가 심함

세션을 사용

✅ 세션을 사용하는 이유

보안상 문제

  • Payload 안의 정보는 암호화가 불가능하고 디코딩이 쉽기 때문에 중요한 정보를 포함할 경우 보안상 위험 존재
  • SNS에서 취미를 공유하고 모임을 만들어 만남으로 이어질 수 있기 때문에, 같은 아이디로 동시에 여러 곳의 접속이 가능하면 위험하다고 판단
    • JWT는 동일한 아이디로 여러 곳에서 로그인이 허용 O / 세션은 허용 X

❗️로컬 세션 선택 시에 발생할 수 있는 문제

데이터 일관성이 깨짐

  1. 세션 정보 불일치

    많은 유저가 이용하는 사이트라 하나의 서버에 과부하를 줄이기 위해 여러 대의 서버를 두는데, 이때 세션 정보 불일치 문제가 발생

  2. 서버 장애 상황 시 세션 데이터 손실

    서버를 재시작하거나 장애 상황이 발생 시에 메모리에 저장되는 세션에 저장된 정보를 손실하는 문제가 발생

⇒ ✅ 데이터의 일관성이 깨지면 비즈니스에 영향을 준다고 판단하여, 세션 ID를 저장할 세션 스토리지를 사용하여 글로벌 세션으로 관리하기로 선택

📌Redis VS Memcached

🤔 세션 스토리지는 어떤 것을 이용해야 하지?

1️⃣ Disk 기반 DB - 성능과 비용 측면에서 사용 X

  • 세션은 빈번하게 읽기 및 쓰기가 발생하는데, 이러한 작업이 디스크에서 이루어지면 메모리보다 속도가 느려 성능 저하가 발생
  • 관계형 데이터베이스는 스케일 아웃보다는 데이터베이스 자체의 성능을 높이는 스케일 업을 해야 하는데, 이 과정에서 다른 2개 방식보다 높은 비용 필요

⇒ In-memory DB 사용을 선택

✅ In-memory DB를 사용하는 이유

  1. 디스크 I/O 부하 감소

    다른 2가지 데이터베이스는 기본적으로 디스크에 데이터를 저장하고 관리하는 반면, 인메모리 DB는 데이터를 메모리에 저장하므로 디스크 I/O 작업이 필요하지 않거나 최소화

    ⇒ 이로 인해 빠른 읽기 및 쓰기가 가능하며 디스크 관련 리소스는 적게 사용

  2. 단순한 데이터 모델

    데이터의 저장 및 검색에 복잡한 인덱싱 및 쿼리 최적화가 필요하지만, 인메모리 DB는 간단한 키-값 형태의 데이터 모델을 가지고 있어 복잡한 구조가 필요 X

🤔 In-memory DB 중 어떤 것을 사용하지? (Redis VS Memcached)

hobby-chain은 사용자가 많고 자주 이용되는 사이트이기 때문에, ‘확장’에 중점으로 두고 비즈니스에 적합한 Redis를 선택

✅ Redis를 사용하는 이유

  1. 확장성

    Memcached의 분산 환경에서는 샤딩을 수행하여 여러 서버에 데이터를 분산 저장하지만, Redis는 마스터-슬레이브 구조가 가능하여 분산 환경에서 효과적인 데이터 관리가 가능

  2. 읽기 위주 작업

    세션 스토리지를 사용할 때 읽기 작업이 60% 이상을 차지할 것이라 생각하여, 읽기 위주에 적합한 리플리케이션을 지원하는 Redis를 사용하기로 선택

  1. 성능
    - 레코드는 모두 10,000 기준이고 속도는 ms, 메모리는 MB 단위입니다
  Write Read Delete
Redis 속도 214 6 1
Memcached 속도 100 14 17
Redis 메모리 3.8 1.3 0
Memcached 메모리 27.2 2.5 2

⇒ 쓰기 작업 속도 외에 모든 측면에서 Redis가 빠르고 적은 리소스를 사용

❗️Redis 사용 시 Fallback 처리

❌ 처리 X

Fallback 처리를 하지 않았을 때, 일부 세션 데이터를 손실할 수 있지만 클라이언트에게 다시 로그인을 요청하여 데이터를 수집하는 것이 fallback 처리를 하기 위해 다른 데이터베이스를 연결하는 것보다 간단하고 비용이 적기에 처리하지 않았습니다

✅ Used Session Storage

hobby-cahin-session drawio

[Refactor] 핵심 로직과 부가적인 로직 분리

  • 로그인 체크, 게시물 체크 등 팔로우, 좋아요 등의 서비스 로직에 공통적으로 들어가는 로직 발견
  • 전체 서비스 로직을 봤을때,
    • 핵심적인 코드가 아니고
    • 서비스 로직이 MemberLoginService 등 다른 서비스에 의존하고 있는 상황 발생

=> ArgumentResolver를 통해 컨트롤러를 거치기 전 필요한 값을 체크하고자 함.

[DB] ERD 설계

ERD 다이어그램
  • 인덱스는 조건에 자주 사용되는 컬럼을 생각하며 설계했습니다.

[설계] 알림 서비스

💡 Issue

배경

Firebase Cloud Message API를 사용하여 프로젝트에 push 기능을 추가

❗️ 알림 push 기능은 실시간으로 처리되지 않아도 서비스에 문제가 없다고 판단

문제

  1. 사용자가 많다고 가정하에 요청 받은 서버에서 처리하고 동기적인 코드로 작동시켰을 때 서버에 트래픽이 증가하고 부하가 갈 것이라 판단
  2. 서버에 장애가 생겼을때, 데이터가 전부 삭제되고 기능이 제대로 이루어지지 않는 문제 발생

해결

✔️ 메세지 큐(RabbitMQ)를 사용

결과

부가적인 기능 알림 서비스를 메세지 큐에 담아 비동기로 작동시켜 서버의 부하를 최소화

서버에 문제가 발생했을 시, 메세지 큐에 데이터를 보존하고 있어서 데이터 유실 최소화

시스템을 분리하여 서버 확장에 용이한 아키텍처 구축

🤔 어떤 MQ를 사용할지?

후보) 1. RabbitMQ 2. Kafka 3. Redis

  1. RabbitMQ

    Kafka에 비해 유연한 라우팅이 가능하고, 비동기식으로 구현이 가능

  2. Kafka

    라우팅에 복잡한 과정 필요

  3. Redis

    메모리에 데이터가 저장되는 인메모리 방식의 데이터 저장소이기 때문에, 메시지 큐로 사용할 경우에는 데이터 손실 위험이 있을 수 있음

→ ✅ Used Message Queue

RabbitMQ
rabbitmq drawio

❗️Fallback 처리

Dead Letter Queue

Consumer(서버)에서 처리하는 과정에 문제가 발생해 알림 보내기를 실패한다면 Dead Letter Queue에 실패한 메시지를 보내고 다시 알림을 보낼 수 있도록 구현했습니다.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.